Working with size classes in Interface Builder

August 16, 2014

Android used to be notorious amongst iOS developers for its practically infinite permutations of interface size. Viewed from the iOS world, this used to look like a problem, because iOS didn’t really provide much in the way of support for building interfaces of different sizes.

If you were building a universal app that supported both iPhone and iPad, there was a tendency to end up with a lot of if deviceType == kIpad-style code.

AutoLayout was the first part of fixing that problem, and the job’s been completed with iOS 8 and size classes. This is probably the least-sexy feature introduced in 8, but it’s definitely one of the more important.

Some quick background on size classes

There are currently two size classes - horizontal and vertical, and each one comes in two sizes - regular and compact. The current orientation of the device can be described as a combination of the sizes:

  • Horizontal regular, vertical regular: iPad in either orientation
  • Horizontal compact, vertical regular: iPhone portrait
  • Horizontal regular, vertical compact: no current device
  • Horizontal compact, vertical compact: iPhone landscape

Storyboards and nib files now support these size classes - so you can think of them as having up to four different layouts contained within the same file. At the bottom of the Interface Builder window, there’s now a control that allows you to switch between each combination:

Every control or AutoLayout constraint can exist in one, several or all of the size classes. This means that you can build interfaces that change depending on device type and/or orientation without any code. Controls can appear or disappear, change size, or change arrangements - all based on the layout that you create in Interface Builder.

Checking size classes

To demonstrate how the size classes change as the device rotates, you can use the a new callback method that’s called as the interface changes:

-(void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
             withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    NSLog(@"Trait collection = %@", newCollection);
}

This will display the trait collection that each orientation triggers:

2014-08-16 19:04:49.785 Adaptr[6522:213857] Trait collection = <UITraitCollection: 0x7fa983c13a10; _UITraitNameUserInterfaceIdiom = Phone, _UITraitNameDisplayScale = 2.000000, _UITraitNameHorizontalSizeClass = Compact, _UITraitNameVerticalSizeClass = Regular, _UITraitNameTouchLevel = 0, _UITraitNameInteractionModel = 1>

How to build adaptive layouts

To demonstrate this, this is a simple universal app that changes layout depending on device and orientation. You can download the full project from here, or follow along. I’m assuming that you’re using Xcode 6 Beta 5 at a minimum.

To start with, create a new universal Single View application. This will create an App Delegate, a view controller and a storyboard. I’m going to ignore the code entirely, and focus on the Storyboard alone (if you’re working with xib files, the process is exactly the same).

Before getting going, it’s worth doing a bit of tweaking to Xcode so that you can see what’s going on. If the Assistant Editor isn’t visible, open it on the right (View -> Assistant Editor -> Assistant Editors on Right) and then switch the Assistant Editor to Preview mode:

This will show a preview of the storyboard in the right hand pane - by default, it’s set up for a 4” iPhone. You can add other devices by clicking on the + icon at the bottom of the preview and selecting the device(s) that you need. They appear side-by-side in the preview pane, so if you haven’t invested in a Thunderbolt Display so far, now would be a good time to do so.

The basic layout

By default, Interface Builder creates a Storyboard with a square ‘Any, Any’ layout - in other words, anything you do with this layout will be common across all devices.

We’re going to start by centering a red block with a constant border. Drag a UIView object into the main view, set the size to 200 x 200, center it in the superview and set the background colour to red. In the Storyboard, it will look like this:

However, if you look at the preview (or run the app in the Simulator) for each device, you’ll see that things look very different:

To fix this, we need to add some AutoLayout constraints. Add constraints to pin the leading, trailing, top and bottom spaces to 50 points:

Run the app again, and this time the red block will be placed correctly regardless of which device you use (you can also change the size of the device in the Resizable Simulator, and the layout will still work.)

Altering constraints in different layouts

So far, we haven’t done anything that previous versions of iOS and Xcode could do. To demonstrate how the layout can change between devices, we’ll alter things so that in landscape, the red block fills the entire screen.

To do this we need to change the layout classes that the constraints are added to. If you select a constraint in the object tree then view the Attributes inspector, you’ll see an Installed checkbox, with a small + to the left:

By default this constraint is installed in all layout classes - what we need to do is to add the appropriate layout classes so that the constraints can be added to the appropriate one. Click the + button, then add a compact width, regular height layout:

When you add the new layout, the constraint will be automatically added to both layouts - deselect the Installed checkbox to remove it from the default layout:

Repeat the same process for the other three constraints. As you remove the constraints from the default layout, you’ll see them disappear from the main Interface Builder pane, and become greyed out in the view tree:

To see the size class where the constraints are active, change it using the selectors at the bottom of the Interface Builder pane:

Once you’ve switched layout, you’ll see them reappear:

If you run the app again in the iPhone Simulator everything will look fine in portrait orientation. Rotate into landscape, though, and things go horribly wrong - the red block disappears, albeit with a nice animation. This is because there are no layout contraints present in this layout class combination, so AutoLayout does its best to figure out what should happen and gets it wrong. To fix this, we need to add constraints for the landscape scenario.

In Interface Builder, switch the layout class to Any width, Compact height:

Now add four new AutoLayout contraints - leading, trailing, top and bottom spaces - and set the constant value for each one to 0. Note that each constraint is added to the Any, Compact layout and not the default:

Run the app again, and as you rotate the Simulator into landscape the red block will be animated to fill the screen:

Adding and removing views in different layouts

As well as adding. removing and changing contraints between different layouts, you can do the same thing with views and controls. This could allow you to build completely different interfaces in portrait and landscape orientations; or as an alternative to separate interfaces for different classes of device.

To illustrate this, we’ll update the current interface to add a white block that appears in landscape:

Start by switching to Any width, Any height and adding a UIView to the interface. Make sure it’s a sibling view of the red block, and set its background colour to white.

Next, change to Any width, Compact height and add constraints to set a 50 point inset on all four edges.

If you run the Simulator now, you’ll see that the white block is the correct size and position in landscape, but the transition between landscape and portrait could be better:

This is because the view animates between the constraints in each size class combination, and at the moment those in portrait are undefined.

To fix this, switch to Compact width, Regular height and add constraints to centre the white block and fix its height and width to zero. You’ll also need to reduce the height and width constraint priorities to 750 to prevent a clash between size and inset constraints.

This fixes the start point of the portrait-to-landscape animation, and the end point of the landscape-to-portrait. Because the start and end points are defined, the transition of the white block is smoothly animated:

Where to go from here

By using size classes and constraints, it’s going to be possible to build up responsive interfaces in a way that wasn’t feasible before iOS8. As well as simplifying the management of device rotation, you can also create universal interfaces - something that should make targeting multiple device types a lot easier.

The other interesting extrapolation here is that we’ve now got all the tools we need to build interfaces for any size of device - whether it’s an iPhone 6 with a large screen; or apps for Apple TV; or even CarPlay. Maybe the long-awaited Apple TV SDK might be on the way…?