Size Classes in Swift

Size classes help you change the layout of views to adapt to various screen sizes, ranging from a tiny iPhone SE to a massive 12.9” iPad Pro. There are 3 different cases for a size class: regular, compact, and unspecified. Size classes are split up into horizontal and vertical classes, allowing you to adapt based on the size of each dimension. In the horizontal case, regular applies to all iPads in full screen as well as some iPhones in landscape, compact applies to all iPhones in portrait and most iPhones in landscape along with some iPad multitasking configurations, and unspecified is the case where it is not yet determined. For a better and more detailed explanation of size classes, check out this great article: https://useyourloaf.com/blog/size-classes/.

Using Size Classes

Using size classes is pretty simple. Every UIView and UIViewController has access to a traitCollection property, which contains both the horizontal and vertical size classes. You can use these when setting up your views to properly lay out for the current display. Typically, you’ll want to just check the horizontal size class, as this is the one that will change when split view is activated on iPad.

Adapting to Changes in Size Classes

It’s also important to properly adjust views when the size class changes. This occurs when your app is used in multitasking split view on iPad or when the device is rotated. This can cause the size class to change from regular to compact.

In order to respond to these changes, your UIView or UIViewController needs to override traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?), which is called every time the trait collection changes. Inside this method, you want to write something like this:

public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    guard previousTraitCollection.horizontalSizeClass != traitCollection.horizontalSizeClass else { return }
        
    // Your layout code here
}

Note that changes in sizes classes aren’t the only reason why this method is called. Whenever any trait changes (such as from dark mode to light mode), this method is called. It’s important to check that the size class actually changed in order to avoid any erroneous layout code that isn’t needed.

Also note that as of iOS 13, traitCollectionDidChange is not always called when a view is loaded. It’s important that you also consider the size class in your initial layout code, if necessary. If you want to avoid duplicate code, you can create a helper function that performs all size class dependent layout code, which you can call in both your initial setup and in the traitCollectionDidChange method.

Auto Layout Constraints

In order to change constraints based on size classes, you would typically have to maintain 2 separate collections of constraints and manually activate and deactivate them in your layout code. With the help of WWLayout though, you don’t have to do that!

By specifying the horizontal or vertical size class that a set of constraints applies to, WWLayout will automatically respond to changes in size classes and activate the correct constraints. Your code will look something like this:

        insetView.layout.fill(.safeArea, inset: 20)
        
        squareView.layout
            .below(topOf: insetView, offset: 10)
            .height(.lessOrEqual, to: insetView.layout.height - 20)
            .width(toHeight: 1.0)
            .size(260, priority: .required - 1)
        
        // portrait
        squareView.layout(verticalSize: .compact)
            .center(in: insetView, axis: .x)
        
        label.layout(verticalSize: .regular)
            .fill(insetView, axis: .x, inset: 20)
            .below(squareView, offset: 20)
        
        // landscape
        squareView.layout(verticalSize: .compact)
            .leading(to: insetView, offset: 10)
        
        label.layout(verticalSize: .regular)
            .top(to: insetView, offset: 20)
            .leading(to: squareView, edge: .trailing, offset: 20)
            .trailing(to: insetView, offset: -20)

Accessing Size Class Outside of UIView and UIViewController

There might be some cases where you want to access the current size class outside of the classes where traitCollection is defined, such as in a model object. While iOS 13 introduced the UITraitCollection.current property, you might need a solution that works on all supported OS versions.

There are 2 ways to go about this. You can either pass along the trait collection from the UIView or you can access it directly by calling UIScreen.main.traitCollection. Either way will get you the same results, so choose whichever way works best with your code.

Moratorium on Device Idioms

Some developers currently use UIDevice.current.userInterfaceIdiom to layout their views based on device. While this might work fine if your app is always fullscreen, it is not compatible with iPad multitasking. The reason is that this variable simply checks the device idiom and returns either phone for iPhones or pad for iPads. It does not tell you the current size class or anything about the actual size of the display, nor does it respond to changes in size. Therefore, it should be avoided unless there is something you specifically need to vary between devices.