In particular, I'm looking at the feature of @Environment where variables can be set on a View, but then all sub-Views have access to the same @Environment. And where different View can have different environment values. I'd like to make a property wrapper that this similar behavior.
I've forgotten about that post, honestly. I'll come around to documenting my findings and posting them.
How It Works
Here's how I think it works after digging around under the hood and experimenting (none of it maintains any global state):
EnvironmentValues
The EnvironmentValues structure has an internal init() that creates a default instance. It has a single private var everyValue: [ObjectIdentifier: Any] inside where the key is constructed using the metadata object of the type conforming to the EnvironmentKey protocol (e.g. Key) and the value is a type-erased instance of Key.Value (self.everyValue[.init(Key.self), default: Key.defaultValue] as! Key.Value). The public subscript merely provides type-safe access to that dictionary.
Environment
The Environment structure has an internal enum Storage with case keyPath(KeyPath<EnvironmentValues, Value>) and case value(Value). It also has a func resolve(with environment: EnvironmentValues) which applies the key path and changes the storage to case value. The wrappedValue returns the direct value or the default value by resolving the key path against a default environment.
View
The View protocol requires the conforming type to be a value type because it's relying on every view to be deep-copyable. If you conform a reference type to it, you'll get an assertion failure.
Before accessing the view's body, the view is copied and using hidden and more powerful reflection, all its @Environment properties are found and their resolve(with:) called with the current environment. After resolving all environment values, the body is accessed and the process recurses into subviews.
The .environment(_:_:) modifier simply wraps the affected view with a view modifier that accesses the current environment and sets the value with the provided key path.
Try This At Home
I've abstracted away the heterogeneous container pattern used by EnvironmentValues into AttributeKit and abstracted away access to the hidden and more powerful reflection into IntrospectionKit.
Wow, okay that's really deep! Thanks for that really detailed explanation! Reflection was not in my mental toolbox before. I'll have to take a look and see if this can be useful for me. Thank you so much!
Hello,
Well, I'm pretty late for this topic but I think it's still very relevant. I've been trying to replicate the Environment propagation feature and this topic was very useful for me to do so. However I didn't have much progress since I found some problems. I presume you use your IntrospectionKit to find the properties wrapped with @Environment? if so how do you infer the generic Value?
Yea, I'm using IntrospectionKit to find all Environment properties, but I'm not looking for them specifically. I'm looking for any mutable property that conforms to the DynamicProperty protocol, making the current EnvironmentValues available through a dedicated TaskLocal variable, then calling the update() method on the DynamicProperty, which changes its own Storage from case keyPath to case value by reaching into the TaskLocal.