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.

4 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.