How does @Environment work internally?

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 see the post from Reverse-Engineering SwiftUI and the included link to Paul Hudson's Global Variable Oriented Programming video. And I've looked at the Open SwiftUI project's implementation of @Environment. But all those implementations have a global variation.

Any thoughts or pointers?

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.

7 Likes

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!

1 Like

I'm very happy to share my findings! :blush:

Great findings :100: I was also curious how it works. I tried to make something similar but for UIKit.

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.

Hope this helps. :slightly_smiling_face:

1 Like

Thank you for the advice! I got so much progress now

1 Like