Mastering Adaptive UI in iOS: Size Classes and UITraitCollection

As iOS developers, we face the challenge of building interfaces that adapt seamlessly to a wide range of devices and form factors. From the 4" iPhone SE to the 12.9" iPad Pro, users expect our apps to look and feel at home on their device of choice. In fact, a recent study by Perfecto found that 44% of users feel frustrated when an app‘s UI doesn‘t adapt well to their device size (Perfecto, 2020).

Designing adaptive interfaces is no longer optional – it‘s essential for creating successful, professional iOS apps. Fortunately, Apple has provided powerful tools for implementing adaptive design since the launch of iOS 8 in 2014. Chief among these are size classes and the UITraitCollection API.

In this article, we‘ll explore how to leverage size classes and UITraitCollection to create iOS UIs that intelligently adapt to any screen size and orientation. I‘ll share code samples, best practices, and tips from my experience as a developer building adaptive iOS apps for clients. Whether you‘re new to iOS or a seasoned pro, I hope you‘ll gain a deeper understanding of this core aspect of modern app development.

The Road to Adaptive UI on iOS

But first, let‘s briefly revisit how iOS has evolved to support adaptive UIs. In the early days of iPhone OS development, we had a single screen size to target. There was no need for adaptive layout – we simply used hard-coded frames and positions that looked pixel-perfect on a 3.5" display.

This began to change in 2010 with the arrival of the iPad and iPhone 4. We now had to consider Retina displays and substantially different screen sizes. Apple‘s initial solution was the autoresizing mask, which let us define simple layout relationships for a view relative to its parent. But as device sizes proliferated, autoresizing masks quickly proved inadequate for building complex, flexible layouts.

iOS 6 introduced Auto Layout in 2012, providing a powerful constraint-based system for defining adaptive layouts. But we still had to consider each screen size and orientation as a special case, implementing different constraints for each scenario.

The advent of size classes and UITraitCollection in iOS 8 brought a major leap forward. We could now define our layout in terms of general categories of screen size – compact and regular – rather than specific devices and orientations. The system would automatically apply the appropriate size classes as the available space changed.

iOS 9 built on this foundation with multitasking features like Split View and Slide Over on iPad. Now, not only did our UIs need to adapt to different devices, but to different configurations of the same device. We could have compact and regular size classes applying simultaneously in different dimensions as a user resized an app in Split View.

Further developments like the 12.9" iPad Pro in 2015 and the edge-to-edge iPhone X in 2017 have only underscored the importance of adaptive UI. Through it all, size classes have remained the bedrock for designing flexible, future-proof interfaces.

Exploring Size Classes

So what exactly are size classes? In simplest terms, a size class is a way to categorize the available space for an app‘s UI along a given dimension. For each of the horizontal and vertical axes, the system assigns one of two possible size classes based on the size of the view:

  • Compact: Constrained space in that dimension, e.g. portrait iPhones for width, landscape iPhones for height
  • Regular: Expansive space in that dimension, e.g. iPads in both dimensions

We can combine the horizontal and vertical size classes to describe the total space available to our UI. For example, a 4" iPhone SE in portrait has a compact width and regular height (wC hR). An iPad in landscape has a regular width and regular height (wR hR).

Here‘s a handy reference showing how devices map to size class combinations:

Device Orientation Size Classes
iPhone Portrait wC hR
iPhone Landscape wC hC
iPad Portrait wR hR
iPad Landscape wR hR

(Apple, 2020)

The real power of size classes is that they allow us to design our UI in terms of these general categories, rather than specific devices. We can define different layouts, constraints, or even view hierarchies for each size class. The system automatically applies the appropriate configuration as the size class changes, whether due to device rotation, Split View, Slide Over, or a new device size entirely.

Accessing Size Classes in Code

To access the current size classes in code, we use the UITraitCollection class. Every UIViewController and UIView has a traitCollection property that describes its current size classes, display scale, and other environmental traits.

We can check the current size class for a given dimension like so:

override func viewDidLoad() {
  super.viewDidLoad()

  if traitCollection.horizontalSizeClass == .compact {
    // Configure UI for compact width
  } else {
    // Configure UI for regular width
  }
}

This lets us programmatically adapt our UI based on the current size classes. For example, we might show a different arrangement of subviews, update font sizes, or load different images.

We can even go beyond layout and use size classes to modify the behavior and capabilities of our UI. Consider an app that displays articles. In a compact width, we might show just the article list. But in a regular width, we could show the list and article content side-by-side.

override func viewDidLoad() {
  super.viewDidLoad()

  if traitCollection.horizontalSizeClass == .regular {
    showDetailView()
  }
}

By modifying the semantic structure of our UI based on size class, we can create experiences that feel tailored to each device size while reusing the same core code.

Responding to Size Class Changes

Size classes are not a static property of a view or view controller. As the available space changes, the system updates the trait collection and notifies the view controller.

We can observe these changes in the traitCollectionDidChange(_:) method:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  super.traitCollectionDidChange(previousTraitCollection)

  if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
    // Horizontal size class has changed, update UI
    updateLayoutForCurrentSizeClass()
  }
}

func updateLayoutForCurrentSizeClass() { ... }

Here, we compare the new trait collection to the previous one to detect changes in a given size class. We can then update our UI accordingly, calling out to a helper method to keep the code organized.

This is a significant change from the earlier days of iOS development. Prior to size classes, we had to rely on methods like willRotateToInterfaceOrientation(_:duration:) to adapt our UI when the device rotated. But with trait collections, the focus is on the available space, not the specific orientation. Our code becomes more declarative and less coupled to device details.

Defining Adaptive Constraints

Perhaps the most common way to adapt our UI to size classes is through Auto Layout constraints. We can define different sets of constraints to be active under different size class conditions.

One approach is to create outlet collections in Interface Builder for each set of size-class-specific constraints:

@IBOutlet var compactConstraints: [NSLayoutConstraint]!
@IBOutlet var regularConstraints: [NSLayoutConstraint]!

override func viewDidLoad() {
  super.viewDidLoad()

  if traitCollection.horizontalSizeClass == .compact {
    NSLayoutConstraint.activate(compactConstraints)
  } else {
    NSLayoutConstraint.activate(regularConstraints)
  }
}

We can then activate and deactivate these constraints in code based on the current size class. This lets us define wholly different layouts for each size class, almost as if we had designed separate storyboards.

I tend to prefer a slightly different approach. Rather than defining separate constraints for each size class, I aim to create a single set of constraints that can adapt to any size class. This often involves using techniques like layout guides, inequalities, content hugging and compression resistance priorities, and stack views.

Consider this simple example of a label and button arranged vertically in a view:

let margin: CGFloat = 20

let stackView = UIStackView(arrangedSubviews: [label, button])
stackView.axis = .vertical
stackView.spacing = margin
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)

NSLayoutConstraint.activate([
  stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
  stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
  stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

Here, the stack view automatically arranges the label and button vertically with a fixed spacing. The stack view itself is center-aligned vertically and pinned to the leading and trailing margins of its parent.

These constraints create an adaptive layout without any explicit reference to size classes. The label and button will stack vertically in compact heights, and expand to fill the available space in regular heights. I find these higher-level constraints easier to reason about and more reusable across devices than size-class-specific constraints.

Of course, there are times when certain constraints should only be active under specific size class conditions. But in general, I aim to start with the simplest constraints possible and only add size-class-specific variations when necessary for the design.

Adaptive UI on Other Platforms

The concepts behind size classes and trait collections are not unique to iOS. Android has a similar system of generalized size buckets and layout qualifiers that adapt to screen sizes and orientations (Android Developers, n.d.). Web developers have long used media queries in CSS to create responsive designs that adapt to viewport sizes.

The specifics of the implementation differ, but the core goals are the same. By describing our UI in terms of general size and orientation categories, rather than specific devices, we can create more flexible, maintainable designs.

One key difference between iOS and responsive web design is the presence of semantic UI differences across sizes. On the web, a responsive design typically presents the same content across all viewport sizes, reflowing and resizing it to fit the space. But on iOS, it‘s common to present different subviews, controls, or even entire screens based on the size class.

For example, a common pattern on iPad is to use a split view controller to display a primary and secondary view side-by-side in a regular width. But in a compact width, the app might display only the primary view, pushing the secondary view onto a separate screen.

This type of semantic adaptation is less common on the web, but I believe it‘s a powerful pattern. By considering not just how our content reflows, but how it‘s structured across different device sizes, we can create interfaces that feel more intentionally designed for each context.

Designing Adaptive UI

When designing an adaptive UI, it‘s important to consider not just how views resize, but how their context and relationships change across size classes.

One classic example is the "Popover" UI idiom on iPad. In a regular width, a popover typically displays additional content or options in a floating panel anchored to its source view. But in a compact width, there may not be sufficient space to display the popover without obscuring the main content. In this case, it‘s often better to transition the popover to a full-screen modal presentation.

Apple‘s Human Interface Guidelines offer extensive guidance on how to adapt common UI patterns across size classes (Apple, 2020). But the key principle is to maintain clarity and coherence as the device size changes. Aim to present a consistent experience that makes sense for each size class context, even if the specific presentation differs.

Practically, this means designing each key screen in your app across a range of size classes from the start. Sketch out how each screen might look on an iPhone, a narrow iPad split view column, and a full-screen iPad. Consider how controls, images, and text content will scale and reflow. Look for opportunities to optimize information density, touch targets, and visual grouping for each size.

It can be tempting to start with a single device size and add adaptive layout as an afterthought. But in my experience, this often leads to subpar experiences on other device sizes. By considering the full range of devices from the beginning, we can create UIs that feel custom-tailored to each environment.

The Future of Adaptive UI on iOS

As the iOS ecosystem continues to evolve, I believe adaptive UI will only become more important. The latest iPad Pro models and Magic Keyboard accessory blur the lines between tablet and laptop interfaces. Multi-window support in iPadOS 13 introduces even more granular size class combinations to consider.

I‘m particularly excited about Mac Catalyst, which lets us bring our iPad apps to macOS with minimal code changes. By leveraging size classes and adaptive layout, we can create apps that feel at home across an incredibly wide range of devices and contexts, from a 4" iPhone SE to a 27" iMac.

At the same time, I believe there‘s still room for improvement in how we approach adaptive UI on iOS. I‘d love to see more tools for previewing our layouts across a matrix of size class combinations, rather than individual devices. I think there‘s an opportunity to define semantic layout APIs that more closely match how we think about our UI in terms of size classes and adaptivity.

But overall, I‘m thrilled with the direction Apple has taken with traits and size classes. It‘s never been easier to create apps that feel custom-built for each device size, while still sharing the vast majority of code. By fully embracing adaptive layout in our designs and code, we can create apps that are more flexible, future-proof, and delightful for our users.

Conclusion

Adaptive UI is no longer a nice-to-have, but a core requirement for creating successful iOS apps. By leveraging size classes and UITraitCollection, we can create flexible, dynamic interfaces that automatically adapt to any device size and context.

When designing adaptive UIs, consider not just how views resize and reflow, but how their roles and relationships evolve across size classes. Aim to optimize information density, touch targets, and visual grouping for each major size class.

In code, prefer defining semantic, adaptive constraints that can automatically adjust to size class changes. Avoid hard-coding device-specific logic, and instead use traits to modify view properties and behaviors dynamically.

Ultimately, the goal of adaptive UI is to create apps that feel custom-built for each device and context. By thoughtfully considering adaptivity from the beginning, we can craft experiences that delight users across the ever-expanding range of iOS devices.

References

Similar Posts