Theming iOS Apps is No Longer Hard

Well, at least not for the same reasons. 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.

To create a custom trait, you define a type conforming to UITraitDefinition which serves as the key for your trait:

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 UIColors 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):

extension UITraitCollection {
    var pureBlackDarkMode: Bool {

One place where UIKit differs slightly from SwiftUI is that trait setters are defined on a separate type: UIMutableTraits. So, one more extension:

extension UIMutableTraits {
    var pureBlackDarkMode: Bool {
        get { self[PureBlackDarkModeTrait.self] }
        set { self[PureBlackDarkModeTrait.self] = newValue }

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.

It’s a pretty minor thing, objectively speaking, but this is a strong contender for my favorite feature of iOS 17.


Comments powered by ActivityPub. To respond to this post, enter your username and instance below, or copy its URL into the search interface for client for Mastodon, Pleroma, or other compatible software. Learn more.

Reply from your instance: