NEW: Join my free 100 Days of SwiftUI challenge today! >>

Using size classes with AnyView type erasure

Paul Hudson    @twostraws   

SwiftUI gives each of our views access to a shared pool of information known as the environment, and we already used it when dismissing sheets. If you recall, it meant creating a property like this:

@Environment(\.presentationMode) var presentationMode

Then when we were ready we could dismiss the sheet like this:

Text("Hello World")
    .onTapGesture {
        self.presentationMode.wrappedValue.dismiss()
    }

This approach allows SwiftUI to make sure the correct state is updated when the view is dismissed – if we attached an @State property to present the sheet, for example, it would be set back to false when the sheet was dismissed.

This environment is actually packed full of interesting things we can read to help make our apps work better. In this project we’re going to be using the environment to work with Core Data, but here I’m going to show you another important use for it: size classes. Size classes are Apple’s thoroughly vague way of telling us how much space we have for our views.

When I say “thoroughly vague” I mean it: we have only two size classes horizontally and vertically, called “compact” and “regular”. That’s it – that covers all screen sizes from the largest iPad Pro in landscape down to the smallest iPhone in portrait. That doesn’t mean it’s useless – far from it! – just that it only lets us reason about our user interfaces in the broadest terms.

To demonstrate size classes in action, we could create a view that has a property to track the current size class and display it in a text view:

struct ContentView: View {
    @Environment(\.horizontalSizeClass) var sizeClass

    var body: some View {
        if sizeClass == .compact {
            return HStack {
                Text("Active size class:")
                Text("COMPACT")
            }
            .font(.largeTitle)
        } else {
            return HStack {
                Text("Active size class:")
                Text("REGULAR")
            }
            .font(.largeTitle)
        }
    }
}

Please try running that in a landscape 12.9-inch iPad Pro simulator so you can get the full effect. At first you should see “REGULAR” displayed, because our app will be given the full screen. But if you swipe upwards gently from the bottom of the simulator screen the dock will appear, and you can drag out something like Safari into the right-hand side of the iPad to enter multi-tasking mode.

Even when our app has only half the screen, you’ll still see our “REGULAR” label appear. But if you drag the splitter to the left – i.e., giving our app only a quarter or so of the available space – now it will change to “COMPACT”.

So, at full screen width we’re in a regular size class, and at half screen width we’re still in a regular size class, but when we go smaller then finally we’re compact. Like I said: it’s broad terms.

Where things get more interesting is if we want to change our layouts depending on the environment. In this situation, it would make more sense to use a VStack rather than a HStack when we’re in a compact size class, however this is trickier than you might think.

First, change the code so that we either return a VStack or a HStack:

if sizeClass == .compact {
    return VStack {
        Text("Active size class:")
        Text("COMPACT")
    }
    .font(.largeTitle)
} else {
    return HStack {
        Text("Active size class:")
        Text("REGULAR")
    }
    .font(.largeTitle)
}

When you build the code you’ll see an ominous error: “Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” That is, the some View return type of body requires that one single type is returned from all paths in our code – we can’t sometimes return one view and other times return something else.

You might think you’re going to be clever, and wrap our whole condition inside another view, such as a VStack, but that doesn’t work either. Instead, we need a more advanced solution called type erasure. I say “advanced” because conceptually it’s very clever and because the implementation of it can be non-trivial, but from our perspective – i.e., actually using it – type erasure is marvelously simple.

First, let’s look at the code – replace your current body code with this:

if sizeClass == .compact {
    return AnyView(VStack {
        Text("Active size class:")
        Text("COMPACT")
    }
    .font(.largeTitle))
} else {
    return AnyView(HStack {
        Text("Active size class:")
        Text("REGULAR")
    }
    .font(.largeTitle))
}

I know that’s quite dense to read, so let me simplify what’s changed:

return AnyView(HStack {
    // ...
}
.font(.largeTitle))

If you build the code again you’ll see it compiles cleanly, and even better it looks great when it runs – the app now smoothly switches between a HStack and a VStack depending on the size class.

What’s changed is that we wrapped both our stacks in a new view type called AnyView, which is what’s called a type erased wrapper.

AnyView conforms to the same View protocol as Text, Color, VStack, and more, and it also contains inside it a view of a specific type. However, externally AnyView doesn’t expose what it contains – Swift sees our condition as returning either an AnyView or an AnyView, so it’s considered the same type. This is where the name “type erasure” comes from: AnyView effectively hides – or erases – the type of the views it contains.

Now, the logical conclusion here is to ask why we don’t use AnyView all the time if it lets us avoid the restrictions of some View. The answer is simple: performance. When SwiftUI knows exactly what’s in our view hierarchy, it can add and remove small parts trivially as needed, but when we use AnyView we’re actively denying SwiftUI that information. As a result, it’s likely to have to do significantly more work to keep our user interface updated when regular changes happen, so it’s generally best to avoid AnyView unless you specifically need it.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5