v6/site/posts/2023-10-02-custom-traits.md

54 lines
2.5 KiB
Markdown
Raw Permalink Normal View History

2023-10-03 02:58:15 +00:00
```
title = "Theming iOS Apps is No Longer Hard"
tags = ["swift"]
date = "2023-10-02 22:37:42 -0400"
short_desc = "Using custom UITraitDefinition traits in iOS 17."
2023-10-03 02:58:15 +00:00
slug = "custom-traits"
```
Well, at least not for the [same reasons](/2023/theming-ios-apps/). I figured I'd write a brief follow-up post, but unless you've been living under a rock, you'll have heard that UIKit gained support for custom traits in `UITraitCollection` with iOS 17. They work very similarly to SwiftUI's environment and are exactly what I wanted.
<!-- excerpt-end -->
To create a custom trait, you define a type conforming to `UITraitDefinition` which serves as the key for your trait:
```swift
struct PureBlackDarkModeTrait: UITraitDefinition {
static let defaultValue = true
static let affectsColorAppearance = true
}
```
The default value is, well, the default value. That is, what will be read when the trait's not explicitly defined in the trait environment. The trait definition also specifies whether this trait can effect the appearance of colors, meaning whether dynamic `UIColor`s should be reëvaluted when the trait's value changes.
Then, you can define an extension on `UITraitCollection` which provides more idiomatic access to the trait (rather than always explicitly looking it up with the type):
```swift
extension UITraitCollection {
var pureBlackDarkMode: Bool {
self[PureBlackDarkModeTrait.self]
}
}
```
One place where UIKit differs slightly from SwiftUI is that trait setters are defined on a separate type: `UIMutableTraits`. So, one more extension:
```swift
extension UIMutableTraits {
var pureBlackDarkMode: Bool {
get { self[PureBlackDarkModeTrait.self] }
set { self[PureBlackDarkModeTrait.self] = newValue }
}
}
```
2023-10-03 03:00:23 +00:00
And with that, I can continue using my custom trait just as I was prior to iOS 17, but without any of the pile of hacks. Reading the trait from a `UIColor`'s `dynamicProvider` closure works exactly as you expect and updates when appropriate.
2023-10-03 02:58:15 +00:00
<aside class="inline">
Another nice hack-eliminating change is the ability to override traits at any point in the view hierarchy, rather than just on view controllers and presentation controllers. So, to actually set this trait for my entire app, I just set it on the root `UIWindow`'s `traitOverrides` and it propagates downward—rather than having to use SPI to get the root presentation controller and modify it's override trait collection.
</aside>
It's a pretty minor thing, objectively speaking, but this is a strong contender for my favorite feature of iOS 17.