Skip to content

Observing Size Classes and Trait Changes

Observing Changes in Size Classes Prior to iOS 17

Before iOS 17, iOS developers relied on size classes to adapt user interfaces to different screen sizes and orientations. Size classes, introduced in iOS 8, allowed apps to respond to changes in the available screen space using the UITraitCollection system. Here's how developers observed and handled size class changes:

Key Concepts

  • Size Classes: Defined as regular or compact for both horizontal and vertical dimensions, resulting in combinations like regular/regular, compact/regular, etc.
  • UITraitCollection: Encapsulated traits like horizontalSizeClass and verticalSizeClass.
  • Trait Environment: Views and view controllers conformed to UITraitEnvironment to respond to trait changes.

Implementation

Developers typically implemented the traitCollectionDidChange(_:) method in a UIViewController or UIView to detect size class changes. This method was called when the trait collection changed, such as during device rotation or split-screen multitasking on iPad.

swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    let horizontalSizeClass = traitCollection.horizontalSizeClass
    let verticalSizeClass = traitCollection.verticalSizeClass
    
    // Update UI based on size classes
    if horizontalSizeClass == .compact {
        // Adjust for compact width (e.g., iPhone portrait)
        configureForCompactWidth()
    } else {
        // Adjust for regular width (e.g., iPad or iPhone landscape)
        configureForRegularWidth()
    }
}

Common Use Cases

  • Adaptive Layouts: Adjusting layouts for different size classes, such as stack views switching from horizontal to vertical.
  • Interface Builder: Using Interface Builder to design adaptive interfaces by specifying constraints or views for specific size classes.
  • Multitasking: Handling split-screen or slide-over modes on iPad, where size classes could change dynamically.

Limitations

  • Granularity: Size classes provided only regular or compact, which sometimes lacked precision for complex layouts.
  • Manual Updates: Developers had to manually check and respond to changes, increasing code complexity.
  • Context Dependency: Size class behavior varied across devices (e.g., iPhone vs. iPad) and orientations, requiring extensive testing.

Observing Trait Changes From iOS 17 Onward

With iOS 17, Apple introduced enhancements to the trait system, providing more flexibility and reducing reliance on size classes alone. The UITraitCollection system was expanded, and new APIs were introduced to observe trait changes more effectively. Here's how developers handle trait changes in iOS 17 and later:

Key Improvements

  • Expanded Traits: Beyond size classes, UITraitCollection now includes additional traits like userInterfaceStyle (light/dark mode), layoutDirection, and more.
  • Trait Observation API: iOS 17 introduced the UITraitChangeObservable protocol and related APIs for more streamlined trait change handling.
  • Dynamic Updates: Improved support for dynamic trait changes, especially in SwiftUI and UIKit interoperability.

Implementation

Instead of relying solely on traitCollectionDidChange(_:), developers can use the registerForTraitChanges(_:as:with:) method to observe specific trait changes. This API allows targeting specific traits and provides a closure-based approach for handling updates.

swift
override func viewDidLoad() {
    super.viewDidLoad()
    
    // Register for horizontal size class changes
    registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: Self, previousTraitCollection: UITraitCollection) in
        let horizontalSizeClass = self.traitCollection.horizontalSizeClass
        if horizontalSizeClass == .compact {
            // Handle compact width
            self.configureForCompactWidth()
        } else {
            // Handle regular width
            self.configureForRegularWidth()
        }
    }
}

For SwiftUI, developers can use the onChange(of:perform:) modifier or environment values to respond to trait changes:

swift
struct ContentView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        VStack {
            if horizontalSizeClass == .compact {
                Text("Compact Layout")
            } else {
                Text("Regular Layout")
            }
        }
        .onChange(of: horizontalSizeClass) { newSizeClass in
            // Handle size class change
            print("Horizontal size class changed to: \(newSizeClass)")
        }
    }
}

Advantages

  • Targeted Observation: Developers can register for specific traits, reducing unnecessary updates.
  • SwiftUI Integration: SwiftUI's environment and onChange modifiers simplify trait-based layout adjustments.
  • Future-Proofing: The new API is more flexible and aligns with Apple's push toward declarative frameworks.

Common Use Cases

  • Dynamic Layouts: Seamlessly adapting to size class changes in SwiftUI or UIKit for responsive designs.
  • Dark Mode: Responding to userInterfaceStyle changes for light/dark mode support.
  • Multitasking and Rotation: Handling dynamic changes in multitasking scenarios or device orientation with less boilerplate code.

Considerations

  • Backward Compatibility: For apps targeting iOS versions before 17, developers must maintain traitCollectionDidChange(_:) implementations.
  • Learning Curve: The new API requires familiarity with closure-based programming and trait-specific registration.
  • Performance: Registering for multiple traits can increase complexity, so developers should optimize registrations.

Summary

  • Pre-iOS 17: Relied on size classes (regular/compact) and traitCollectionDidChange(_:) for adaptive layouts, with manual checks for changes.
  • iOS 17 Onward: Introduced registerForTraitChanges(_:as:with:) and enhanced SwiftUI support for more granular and efficient trait change observation.
  • Migration: Developers supporting both old and new iOS versions should implement conditional logic to use the new API when available while maintaining compatibility with older methods.

This evolution reflects Apple's focus on simplifying adaptive UI development while providing more precise control over trait changes.

Released under the MIT License.