From c1a92540c0177db391ebe5bdb7622d791dd2eef6 Mon Sep 17 00:00:00 2001 From: srujanc <77649680+srujanc@users.noreply.github.com> Date: Sat, 11 Nov 2023 00:12:23 +0530 Subject: [PATCH 1/2] WWDC2023 10057 - Unleash the UIKit trait system --- content/notes/wwdc23/10057.md | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 content/notes/wwdc23/10057.md diff --git a/content/notes/wwdc23/10057.md b/content/notes/wwdc23/10057.md new file mode 100644 index 00000000..23f7ed07 --- /dev/null +++ b/content/notes/wwdc23/10057.md @@ -0,0 +1,117 @@ +--- +contributors: srujanc +--- + +# UIKit Trait System + +UIKit provides many built-in system traits, such as user interface style, horizontal size class, and preferred content size category. In iOS 17, We can define our own custom traits as well. This unlocks a powerful new way to provide data to the app's view controllers and views. The main way to work with traits in UIKit is using [trait collections](https://developer.apple.com/documentation/uikit/uitraitcollection?language=objc). + + +## UITraitCollection + +A trait collection contains traits and their associated values. The traitCollection property of the [UITraitEnvironment](https://developer.apple.com/documentation/uikit/uitraitenvironment?language=objc) protocol contains traits that describe the state of various elements of the iOS user interface, such as size class, display scale, and layout direction. Together, these traits compose the UIKit trait environment. + + +There are some new APIs in iOS 17 that make it easier to work with trait collections. + +*** Initialize a New Closure *** + +``` swift + +// Build a new trait collection instance from scratch +let myTraits = UITraitCollection { mutableTraits in + mutableTraits.userInterfaceIdiom = .phone + mutableTraits.horizontalSizeClass = .regular +} + +``` + +From the above code, there is a mutableTraints variable in side the closure which conforms to a new protocol called [UIMutableTraits](https://developer.apple.com/documentation/uikit/uimutabletraits). When the closure finishes executing, the initializer returns an immutable UITraitCollection instance that contains all of the trait values I set inside the closure. + +There’s also a new modifyingTraits method that allows you to create a new instance by modifying values from the original trait collection inside the closure. + +``` swift + +// Get a new instance by modifying traits of an existing one +let otherTraits = myTraits.modifyingTraits { mutableTraits in + mutableTraits.horizontalSizeClass = .compact + mutableTraits.userInterfaceStyle = .dark +} + +``` + +Each trait definition has an associated value type, which is inferred from the defaultValue. + + +### UIMutableTraits + +The UIMutableTraits protocol provides read-write access to get and set trait values on an underlying container. UIKit uses this protocol to facilitate working with instances of UITraitCollection, which are immutable and read-only. The UITraitCollection initializer init(mutations:) uses an instance of UIMutableTraits, which enables you to set a batch of trait values in one method call. UITraitOverrides conforms to UIMutableTraits, making it easy to set trait overrides on trait environments such as views and view controllers. + + +``` swift + +// ThemeTrait conforms to UITraitDefinition, and has a defaultValue type of Theme +extension UIMutableTraits { + var theme: Theme { + get { self[ThemeTrait.self] } + set { self[ThemeTrait.self] = newValue } + } +} + +// Apply an override for the custom theme trait. +view.traitOverrides.theme = .monochrome + +``` + +Trait overrides are the mechanism you use to modify data within the trait hierarchy. In iOS 17, it’s easier than ever to apply trait overrides. There’s a new traitOverrides property on each of the trait environment classes, including window scenes, windows, views, view controllers, and presentation controllers. + +> Trait overrides applied to the parent affect the parent’s own trait collection. And then the values from the parent’s trait collection are inherited to the child. Finally, the child's trait overrides are applied to the values it inherited to produce its own trait collection. + + +``` swift + +/// 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 + } +} + +``` + +[!Note] traitCollectionDidChange is deprecated in iOS 17. When you implement traitCollectionDidChange, the system doesn’t know which traits you actually care about, so it has to call that method every time that any trait changes value. However, most classes only use a handful of traits and don’t care about changes to any others. This is why traitCollectionDidChange doesn’t scale as you add more and more custom traits. + + +In Place of traitCollectionDidChange we have new traits registration methods in iOS 17.0 with a closure-method based. + +> Call registerForTraitChanges and pass an array of traits to register for as well as the target and action method to call on changes. The target parameter is optional. If you omit it, the target will be the same object that registerForTraitChanges is called on. + + +``` swift + +// 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... +} + +``` + +The first parameter is always the object whose traits are changing. Use this parameter to get the new traitCollection. The second parameter will always be the previous trait collection for that object before the change. In addition to registering for individual traits, you can also register using new semantic sets of system traits. From e7f1d28d0c153521baaf853845f09e8897b3840b Mon Sep 17 00:00:00 2001 From: srujanc <77649680+srujanc@users.noreply.github.com> Date: Sat, 11 Nov 2023 09:07:45 -0700 Subject: [PATCH 2/2] Update 10057.md --- content/notes/wwdc23/10057.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/notes/wwdc23/10057.md b/content/notes/wwdc23/10057.md index 23f7ed07..2a9905b4 100644 --- a/content/notes/wwdc23/10057.md +++ b/content/notes/wwdc23/10057.md @@ -84,7 +84,7 @@ func toggleThemeOverride(_ overrideTheme: MyAppTheme) { ``` -[!Note] traitCollectionDidChange is deprecated in iOS 17. When you implement traitCollectionDidChange, the system doesn’t know which traits you actually care about, so it has to call that method every time that any trait changes value. However, most classes only use a handful of traits and don’t care about changes to any others. This is why traitCollectionDidChange doesn’t scale as you add more and more custom traits. +**Note:** traitCollectionDidChange is deprecated in iOS 17. When you implement traitCollectionDidChange, the system doesn’t know which traits you actually care about, so it has to call that method every time that any trait changes value. However, most classes only use a handful of traits and don’t care about changes to any others. This is why traitCollectionDidChange doesn’t scale as you add more and more custom traits. In Place of traitCollectionDidChange we have new traits registration methods in iOS 17.0 with a closure-method based.