How does the SwiftUI environment work, and when should you use it instead of passing a parameter?
Predict first
What does a custom environment value return before and after being set?
import SwiftUI
struct Key: EnvironmentKey { static let defaultValue = 7 }
extension EnvironmentValues { var myKey: Int { get { self[Key.self] } set { self[Key.self] = newValue } } }
var env = EnvironmentValues()
print(env.myKey)
env.myKey = 9
print(env.myKey)
- 0, then 9
- 7, then 9
- 7, then 7
Mental model. The environment is the weather and the building’s rules: everyone inside sees the same temperature without being handed it, but you would not use it to hand someone their coat.
The environment is a set of values that flows down the view tree: system values (colorScheme, dynamicTypeSize, layoutDirection, dismiss, openURL) plus your own. A view reads one with @Environment(\.colorScheme), and a parent sets one for a whole subtree with .environment(\.myKey, value). Values are inherited, and a deeper view can override them for its own subtree. A custom value is a key on EnvironmentValues with a default, defined with an EnvironmentKey (or the @Entry macro), and reading it in a view where nothing was set gives the default. With Observation you can also place a model in the environment by type: .environment(model) and @Environment(Model.self).
Use it for cross-cutting context that many views at different depths need and that rarely changes: theme, locale, an app-wide service or the current user. Do not use it to avoid passing the one value a child plainly needs: explicit parameters keep dependencies visible, are easier to test and preview, and read as documentation. An environment value read in body is a dependency like any other, so changing it re-evaluates readers; reading a model from the environment and then reading its properties tracks those properties. A missing value for an object type crashes at the point of reading, which is the usual bug when a preview or a sheet forgets to inject it (sheets inherit the environment, but a separate window or hosting controller does not).
An unset key returns its default, and a set value replaces it for that environment.
Common traps
- Reading an environment object that was never injected. It crashes; inject in previews and at scene roots.
- Using it as a global variable. Hidden dependencies make views hard to test and reason about.
They might follow up with
- Does a sheet see the environment? Yes, presented views inherit it, but a new hosting controller or window does not.
- How do you add your own value? An
EnvironmentKeywith a default and an extension onEnvironmentValues, or the@Entrymacro.
Related questions
- 8.3.1 What do @State, @Binding and @Bindable do, and who owns the data?
- 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
- 8.9.1 How do #Preview and the macros SwiftUI uses work, and what makes a view easy to preview?
← What do @State, @Binding and @Bindable do, and who owns the data? How does the Observation framework differ from ObservableObject, and… →