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
orcompact
for both horizontal and vertical dimensions, resulting in combinations like regular/regular, compact/regular, etc. - UITraitCollection: Encapsulated traits like
horizontalSizeClass
andverticalSizeClass
. - 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.
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
orcompact
, 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 likeuserInterfaceStyle
(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.
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:
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
) andtraitCollectionDidChange(_:)
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.