Search code examples
iosuikit

Is it possible to change a custom trait from a parent trait collection and observe the changes to all view controllers?


I'm trying to create a custom trait object and put it into the UITraitCollection class using some new APIs in iOS 17. As an example, what I want to do is create multiple app theme traits, put them into the trait collection class, and make all of the current available UIViewController objects to observe the theme trait changes. I'm aware that in iOS 17, UIKit has a new method to override a trait definition by mutating the new traitOverrides property. But based on the WWDC23 video session, Apple told us to use this property only for specific view controllers or views and all of its children/subviews. So the changes don't reflect to all of the view controllers that are currently available in memory. Here's what I did so far:

Creating a new custom UITraitDefinition:

enum ThemeTraitType: Int {
    case light
    case dark
    case monochrome
}

struct ThemeTrait: UITraitDefinition {
    typealias Value = ThemeTraitType
    
    static let defaultValue: ThemeTraitType = .light
    static var name: String = "Theme"
    static var affectsColorAppearance: Bool = true
}

Added it into the UITraitCollection class using extensions:

extension UITraitCollection {
    var currentTheme: ThemeTraitType { self[ThemeTrait.self] }
}

extension UIMutableTraits {
    var currentTheme: ThemeTraitType {
        get { self[ThemeTrait.self] }
        set { self[ThemeTrait.self] = newValue }
    }
}

Register the trait changes observer:

final class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        registerForTraitChanges([ThemeTrait.self], handler: { (self: Self, previousTraitCollection: UITraitCollection) in
            // Do something here...
        })
    }

}

Change the currentTheme trait using the new traitOverrides property:

traitOverrides.currentTheme = .dark

This works as expected for the view controller where I change the trait from. But let's say I'm using a UINavigationController and have 5 view controllers inside the navigation controller and I change the trait inside the last view controller (which is the 5th view controller inside the stack), when I go back to the previous view controller, the trait changes doesn't reflect on all of the previous view controllers. What I want to do is the changes I've made inside any view controllers will also reflect to all of the available view controllers that currently exist in memory. Is this possible to implement? Thank you.


Solution

  • The UIKit trait system is a mechanism to propagate data through the tree structure of your app's hierarchy. You can see illustrations of the trait hierarchy in the WWDC video you mentioned — but the general idea is that it starts at each UIWindowScene, and includes all UIWindow instances that are connected to that window scene, and then within each window it is based on the UIView instances in the view hierarchy. Any UIViewController whose view has been added to the view hierarchy (meaning view.window != nil) is a part of the trait hierarchy.

    However, an object (e.g. view, view controller, etc) that has been created and is in-memory is not necessarily a part of the trait hierarchy at any point in time. For example, a view or view controller that was newly created which hasn't been added into the hierarchy yet (e.g. adding the view using addSubview, or by presenting the view controller, etc) is not a part of the trait hierarchy yet. Similarly, if a view controller has its view removed from the hierarchy (e.g. a presentation is dismissed, or another view controller is pushed on a navigation stack causing the previous view controller to disappear), then that view controller is no longer a part of the trait hierarchy until its view gets put back into the hierarchy later (if ever).

    Data only flows through the trait system to objects that are a part of the trait hierarchy. This is very important, as the exact values received by any given object is dependent on where it appears within that hierarchy, because traits are inherited from parent to child. It's impossible to know with certainty which trait values an object should receive, if its parent is not known because it hasn't been added to the hierarchy yet.

    Therefore, the trait system is not designed to propagate values to the trait collection of objects that aren't in the hierarchy. If/when the object is added to the hierarchy later on, it will receive updated trait values at that time.

    The most important question you should ask yourself here is this: why do you need objects that are not in the hierarchy to receive this data? By definition, an object that is not a part of the hierarchy cannot be visible to the user, and thus it should not need to update until some point in the future if/when it gets added to the hierarchy (at which point it will receive updated trait values). Any objects that have incorrect or stale trait values will always get a chance to update when they get put into the hierarchy, before the user can see them.

    If you truly need to broadcast the same information to all objects in memory, then the trait system is probably not the right choice of infrastructure to use for that use case. Instead, you might consider something like posting a notification via notification center.