Skip to content

Commit

Permalink
Add "How Environment works" article. (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
jverkoey authored Aug 2, 2024
1 parent 71ffb6c commit 27865f6
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# How @Environment works

How the @Environment property enables read/write access to environment values.

The ``Environment`` property wrapper enables views to read environment values
based on the context the view is being rendered in, and the ``View/environment(_:_:)``
modifier allows views to mutate the environment for a sub-tree of views.

Reading values from ``Environment`` is fairly straightforward; the property
wrapper simply returns the requested key value property from the property wrapper's
internal ``EnvironmentValues`` storage.

But how do we make sure that the ``EnvironmentValues`` reflect our current
context's environment values?

Consider a typical example where we inject an environment value into a view
instance:

```swift
MyView()
.environment(\.path, "/home")
```

And then read that value within the view instance:

```swift
struct MyView: View {
@Environment(\.path) var path

var body: some View {
Text(path)
}
}
```

When MyView is rendered, ``View/render(_:environment:)`` is invoked with the
parent context's ``EnvironmentValues``, which has had the `.path` property
set to "/home". But how does `path`, which is being read from within `body`,
know to use the given parent context?

This is where the default implementation of ``View/render(_:environment:)``
comes into play.

## Type introspection and value injection

``View``'s default implementation of ``View/render(_:environment:)`` includes
a method called `injectEnvironment` that has the following signature:

```swift
func injectEnvironment(environment: EnvironmentValues) throws -> Self
```

Note that it returns an instance of Self; in this case, a copy of self with
the given environment values injected into any `@Environment` property
wrappers.

How does this method know which properties are property wrappers? Using
type introspection.

Swift provides the [`Mirror`](https://developer.apple.com/documentation/swift/mirror)
type as a way to perform read-only type introspection, but we need to be
able to mutate the property wrappers so this API unfortunately doesn't meet
our needs. Instead, we need to rely on three private Swift APIs:

- `swift_reflectionMirror_recursiveCount`: Returns the number of properties within in a Type.
- `swift_reflectionMirror_recursiveChildMetadata` Returns traits, such as mutability, of a property within in a Type.
- `swift_reflectionMirror_recursiveChildOffset` Returns the in-memory offset of the property's storage, allowing for reading/writing of the property's value.

These three APIs are wrapped up in Slipstream's package-visible `TypeIntrospection`
API, originally forked from [gor-gyolchanyan-swift/introspection-kit](https://github.com/gor-gyolchanyan-swift/introspection-kit).
`TypeIntrospection` gives us a mechanism for enumerating each of the properties
contained within a type, in this case our View type, and to then mutate them on
a copied instance of the view.

When we render the view, we create a copy, inject the current environment values
into any detected ``Environment`` property wrappers, and then invoke the view's
``View/body``. This ensures that when the view's body logic runs, the correct
environment values have been injected into the view.

[View the full source for this solution in View.swift](https://github.com/jverkoey/slipstream/blob/main/Sources/Slipstream/Views/Fundamentals/View.swift).
1 change: 1 addition & 0 deletions Sources/Slipstream/Documentation.docc/Slipstream.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ print(try renderHTML(HelloWorld()))
### Architecture

- <doc:HowSlipstreamWorks>
- <doc:HowEnvironmentWorks>

### Data and storage

Expand Down
1 change: 1 addition & 0 deletions Sources/TypeIntrospection/Properties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension TypeIntrospection.Properties: Collection {
}

extension TypeIntrospection.Properties {
/// Source: https://github.com/swiftlang/swift/blob/9906199d8ef5408ab63663bf6f6fb1f2511edf4f/stdlib/public/core/ReflectionMirror.swift#L32-L33
@_silgen_name("swift_reflectionMirror_recursiveCount")
private static func _getRecursiveChildCount(_in type: Any.Type) -> Int
}

0 comments on commit 27865f6

Please sign in to comment.