How does a SwiftUI View's body get re-evaluated, and what does that mean for state design?
SwiftUI doesn't update a screen — it re-asks the same question, diffs the answer, and writes only the delta; understanding that cycle is the difference between fighting the framework and designing state that flows with it.
LEARN BY DOINGPut your intuition to work3 steps
How does a SwiftUI View's body get re-evaluated, and what does that mean for state design?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.The honest, slightly uncomfortable answer to give first: a SwiftUI view's body is not a method you call, it is a pure computed property that the SwiftUI runtime calls, as many times as it decides to, whenever it believes state relevant to that view has changed. You do not control when it runs. You only control what it returns, and how expensive that return is to compute. Interviewers ask this question not to hear the property-wrapper table recited back — they want to know whether you can explain why a screen "flashes" when one row's timestamp ticks, or why an animation restarts for no visible reason. Both of those are direct consequences of the mechanism below.
"Relevant" state change means one of a small set of triggers: a @State-owned value type mutating, a @Binding write (which forwards to whatever @State or @Published property is the real source of truth), an @ObservedObject or @StateObject's wrapped object publishing through objectWillChange (fired automatically by every @Published property on it), an @EnvironmentObject or @Environment value changing, or — since the Observation framework arrived in iOS 17 — an @Observable-macro type whose specific property was actually read the last time that body executed.
Once a view is marked invalid, here is what actually happens, end to end:
Two stages matter here, and conflating them is the most common mistake. The first stage — calling body and getting back a tree of View values — is cheap by construction: nothing on screen has been touched yet, you have just produced a value-type description, mostly structs, of what the screen should look like. The second stage — the reconciler's diff against the previous tree, and only then writing the handful of attributes that actually changed down to the real render tree — is where the expensive work is gated. This two-stage design is exactly why SwiftUI can afford to call body far more liberally than a UIKit view would ever call layoutSubviews: the diff, not the call, is the checkpoint.
The diff itself runs on structural identity: by default, a node's identity is its type plus its position among its parent's children. Two consecutive renders that put a Text in the same slot are treated as "the same" Text, even though a brand-new Text struct was allocated to describe it. You can override this default with an explicit .id(_:). Get this wrong in either direction and the bug is very specific: give two branches of an if/else the same explicit id when they are semantically different views, and SwiftUI can carry state across them that should not have survived; conversely, reorder a list without giving its rows a stable identity tied to the data, and SwiftUI treats every row as brand-new on each reorder — @State resets to its initial value, .onAppear refires, an in-flight animation restarts from nothing.
This is where state design stops being trivia and starts having real consequences. ObservableObject's objectWillChange cannot tell you which @Published property actually changed — it fires for any of them, which means a view that reads only user.name from a shared object still re-renders every time an unrelated user.lastActiveAt ticks once a second. The @Observable macro fixes exactly this by instrumenting property access: a view is invalidated only by the specific properties it actually read while its body last ran, so the same per-second tick on a field the view never touches produces zero work for it. Migrating a large, widely shared ObservableObject to @Observable is one of the highest-leverage, lowest-risk performance fixes available in a mature SwiftUI codebase, because it changes nothing at the call site — only the invalidation granularity.
The practical rules that fall out of all of this are short enough to say out loud in an interview: keep @State as local as the concern it represents, because anything promoted into a shared object widens who gets invalidated; assume any @Published field change on a classic ObservableObject invalidates every observer, so either split large objects by concern or move to @Observable; and keep body itself cheap — no heavy allocation, no synchronous decoding, no date formatting inside a loop — because it genuinely can run more often than the naive mental model expects, and none of the diffing machinery downstream saves you from doing expensive work upstream of it.
🧠 Check your understanding
0/3 · 0/3 answered1. A view observes an ObservableObject with two @Published properties, title and progress. The view's body only reads title. What happens when progress changes?
2. What does SwiftUI primarily use to decide whether a view in a new body call's output is the same view as one from the previous call?
3. Why is it considered cheap for SwiftUI to call a view's body far more often than a UIKit view would ever call layoutSubviews?