How does SwiftUI decide to re-run body, and how do you keep updates cheap?
Predict first
A view reads only model.title from an @Observable model. model.count changes. Does that view’s body re-run?
// A: yes, any property change invalidates it
// B: no, it only depends on the properties it read
// C: only if the view is on screen
- A
- B
- C
Mental model. A spreadsheet recalculating: it re-computes only the cells whose inputs changed. A cell that reads the whole sheet recalculates every time anything changes.
SwiftUI maintains a graph of attributes behind your views. When a view’s body reads a piece of state (a @State value, a binding, an environment value, or, with Observation, a property of an @Observable object), the graph records that dependency. When that source changes it is marked invalid, and on the next update pass (batched, about once per frame, on the main thread) SwiftUI re-evaluates body of the invalid views, diffs the new description against the previous one, and updates only the parts of the render tree that differ. A parent re-evaluating does not automatically re-run a child’s body: a child is skipped if its inputs compare equal to last time, which is why small views with simple stored properties are cheap.
Keeping it cheap follows from that. Narrow dependencies: pass a child only the value it needs, not the whole model, and split a big view so only the part that reads a changing value is invalidated. With Observation, a view re-runs only for the properties it actually read; with ObservableObject, any @Published change invalidates every observing view. Keep body fast and pure: no sorting, formatting or filtering of large data inside it; compute that when the model changes and store the result. Avoid creating expensive objects in body (formatters, large arrays). Make child views Equatable when their inputs are not automatically comparable. Use Self._printChanges() to see why a view updated, and the SwiftUI template in Instruments to count updates and see their cost. Remember that identity changes (a changing .id or a different branch) rebuild a whole subtree.
Observation records which properties body read. A change to another property does not invalidate it (this is checked in the Observation question).
Common traps
- Passing a whole model object to a small view. With
ObservableObjectit re-runs on any change; pass the single value instead. - Doing sorting or formatting inside
body. It runs on every update.
They might follow up with
- How do you find out why a view updated?
let _ = Self._printChanges()inbody, and the SwiftUI Instruments template. - Does a parent update always update its children? No: a child whose inputs compare equal is skipped.
Related questions
- 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
- 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
- 8.1.1 What is a View, what does some View mean, and why do modifiers wrap rather than mutate?