-
Unleash the UIKit trait system
Discover powerful enhancements to the trait system in UIKit. Learn how you can define custom traits to add your own data to UITraitCollection, modify the data propagated to view controllers and views with trait override APIs, and adopt APIs to improve flexibility and performance. We'll also show you how to bridge UIKit traits with SwiftUI environment keys to seamlessly access data from both UIKit and SwiftUI components in your app.
Chapitres
- 0:57 - Understanding traits
- 7:44 - Defining custom traits
- 14:31 - Applying overrides
- 19:48 - Handling changes
- 25:24 - SwiftUI bridging
Ressources
Vidéos connexes
WWDC23
-
Rechercher dans cette vidéo…
-
-
1:51 - Working with trait collections
// Build a new trait collection instance from scratch let myTraits = UITraitCollection { mutableTraits in mutableTraits.userInterfaceIdiom = .phone mutableTraits.horizontalSizeClass = .regular } // Get a new instance by modifying traits of an existing one let otherTraits = myTraits.modifyingTraits { mutableTraits in mutableTraits.horizontalSizeClass = .compact mutableTraits.userInterfaceStyle = .dark } -
9:06 - Implementing a simple custom trait
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } let traitCollection = UITraitCollection { mutableTraits in mutableTraits[ContainedInSettingsTrait.self] = true } let value = traitCollection[ContainedInSettingsTrait.self] // true -
10:23 - Implementing a simple custom trait with a property
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } extension UITraitCollection { var isContainedInSettings: Bool { self[ContainedInSettingsTrait.self] } } extension UIMutableTraits { var isContainedInSettings: Bool { get { self[ContainedInSettingsTrait.self] } set { self[ContainedInSettingsTrait.self] = newValue } } } let traitCollection = UITraitCollection { mutableTraits in mutableTraits.isContainedInSettings = true } let value = traitCollection.isContainedInSettings // true -
11:00 - Implementing a custom theme trait
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true static let name = "Theme" static let identifier = "com.myapp.theme" } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } } -
12:33 - Using a custom theme trait
let customBackgroundColor = UIColor { traitCollection in switch traitCollection.myAppTheme { case .standard: return UIColor(named: "StandardBackground")! case .pastel: return UIColor(named: "PastelBackground")! case .bold: return UIColor(named: "BoldBackground")! case .monochrome: return UIColor(named: "MonochromeBackground")! } } let view = UIView() view.backgroundColor = customBackgroundColor -
18:05 - Managing trait overrides
func toggleThemeOverride(_ overrideTheme: MyAppTheme) { if view.traitOverrides.contains(MyAppThemeTrait.self) { // There's an existing theme override; remove it view.traitOverrides.remove(MyAppThemeTrait.self) } else { // There's no existing theme override; apply one view.traitOverrides.myAppTheme = overrideTheme } } -
21:00 - Trait change handling on older iOS versions
// Efficient implementation that only updates when necessary override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { updateViews(sizeClass: traitCollection.horizontalSizeClass) } } func updateViews(sizeClass: UIUserInterfaceSizeClass) { // Update views for the new size class... } -
21:28 - Registering for trait changes using a closure
// Register for horizontal size class changes on self registerForTraitChanges( [UITraitHorizontalSizeClass.self] ) { (self: Self, previousTraitCollection: UITraitCollection) in self.updateViews(sizeClass: self.traitCollection.horizontalSizeClass) } // Register for changes to multiple traits on another view let anotherView: MyView anotherView.registerForTraitChanges( [UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self] ) { (view: MyView, previousTraitCollection: UITraitCollection) in // Handle the trait change for this view... } -
22:48 - Registering for trait changes using a target-action
// Register for horizontal size class changes on self registerForTraitChanges( [UITraitHorizontalSizeClass.self], action: #selector(UIView.setNeedsLayout) ) // Register for changes to multiple traits on another view let anotherView: MyView anotherView.registerForTraitChanges( [UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self], target: self, action: #selector(handleTraitChange(view:previousTraitCollection:)) ) @objc func handleTraitChange(view: MyView, previousTraitCollection: UITraitCollection) { // Handle the trait change for this view... } -
24:20 - Registering for changes to system traits affecting color appearance
registerForTraitChanges( UITraitCollection.systemTraitsAffectingColorAppearance, action: #selector(handleColorAppearanceChange) ) @objc func handleColorAppearanceChange() { // Handle the color appearance trait changes... } -
24:37 - Manually unregistering for trait changes
// Store the returned registration token let registration = registerForTraitChanges([UITraitHorizontalSizeClass.self], action: #selector(handleTraitChange)) // Later, use the stored registration token to manually unregister unregisterForTraitChanges(registration) @objc func handleTraitChange() { // Handle the trait change... } -
26:19 - Implementing a bridged UIKit trait and SwiftUI environment key
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } // Custom UIKit trait struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } } // Custom SwiftUI environment key struct MyAppThemeKey: EnvironmentKey { static let defaultValue = MyAppTheme.standard } extension EnvironmentValues { var myAppTheme: MyAppTheme { get { self[MyAppThemeKey.self] } set { self[MyAppThemeKey.self] = newValue } } } // Bridge SwiftUI environment key with UIKit trait extension MyAppThemeKey: UITraitBridgedEnvironmentKey { static func read(from traitCollection: UITraitCollection) -> MyAppTheme { traitCollection.myAppTheme } static func write(to mutableTraits: inout UIMutableTraits, value: MyAppTheme) { mutableTraits.myAppTheme = value } } -
27:01 - Setting a UIKit trait and reading the bridged environment value from SwiftUI
// UIKit trait override applied to the window scene let windowScene: UIWindowScene windowScene.traitOverrides.myAppTheme = .monochrome // Cell in a UICollectionView configured to display a SwiftUI view let cell: UICollectionViewCell cell.contentConfiguration = UIHostingConfiguration { CellView() } // SwiftUI view displayed in the cell, which reads the bridged value from the environment struct CellView: View { @Environment(\.myAppTheme) var theme: MyAppTheme var body: some View { Text("Settings") .foregroundStyle(theme == .monochrome ? .gray : .blue) } } -
28:16 - Setting a SwiftUI environment value and reading the bridged trait from UIKit
// SwiftUI environment value applied to a UIViewControllerRepresentable struct SettingsView: View { var body: some View { SettingsControllerRepresentable() .environment(\.myAppTheme, .standard) } } final class SettingsControllerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> SettingsViewController { SettingsViewController() } func updateUIViewController(_ uiViewController: SettingsViewController, context: Context) { // Update the view controller... } } // UIKit view controller contained in the SettingsControllerRepresentable class SettingsViewController: UIViewController { override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() title = settingsTitle(for: traitCollection.myAppTheme) } func settingsTitle(for theme: MyAppTheme) -> String { switch theme { case .standard: return "Standard" case .pastel: return "Pastel" case .bold: return "Bold" case .monochrome: return "Monochrome" } } }
-