If a parent changes one environment value, which child views re-run their body?
Predict first
Which bodies run after the level changes?
@Observable final class Driver { var v = 0 }
let driver = Driver()
extension EnvironmentValues { @Entry var level = 0 }
struct Top: View { var body: some View { let _ = note("Top.body"); Color.clear.frame(height: 1) } }
struct A: View {
@Environment(\.level) var level
var body: some View { let _ = note("A.body level=\(level)"); Color.clear.frame(height: 1) }
}
struct B: View {
@Environment(\.colorScheme) var scheme
var body: some View { let _ = note("B.body"); Color.clear.frame(height: 1) }
}
struct R: View {
var body: some View { VStack { Top(); A(); B() }.environment(\.level, driver.v) }
}
_ = show(R()); flush()
driver.v = 1; spin(); flush()
- first render runs all three, the update runs only A
- the update runs all three
- the update runs A and Top
Mental model. The environment is a wall of labelled switches: flipping the “level” switch only wakes the people who were watching that switch.
The environment is tracked per key. In the check the parent sets a custom level value (declared with the @Entry macro) around three children. When level changes, only A, the view that reads \.level, re-evaluates; B (which reads \.colorScheme) and the view that reads nothing are not re-run. So, unlike passing a whole model down as a parameter, a changed environment value invalidates only the readers of that key, and a child that merely sits below the modifier is untouched.
Practical consequences: a theme or a feature-flag struct in a single key makes every reader re-run when any field of it changes, so split frequently changing values into their own keys, or put an @Observable object in the environment and let property-level tracking narrow the updates. Remember what the environment cannot do: values flow down only (children cannot write up, that is what preferences are for), and an environment object that was never injected crashes where it is read. Define defaults on the key so previews and tests work without setup.
Environment reads are tracked per key.
Common traps
- One big settings struct in a single key. Any field change re-runs every reader.
- Expecting a child to write back through the environment. It flows down only.
They might follow up with
- How do you narrow the updates for a shared model? Put an
@Observableobject in the environment. - How do values go upward instead? Preference keys.
Related questions
- 8.3.2 How does the SwiftUI environment work, and when should you use it instead of passing a parameter?
- 8.8.1 What is a PreferenceKey, and how do values travel from children up to ancestors?
- 8.2.1 How does SwiftUI decide to re-run body, and how do you keep updates cheap?
- 8.10.3 What exactly does Observation track: computed properties, ignored properties, collections, nested objects and…
← When exactly does ObservableObject publish, and why do nested objects… How does @AppStorage behave: where is the value, what happens when it… →