How does SwiftUI diffing work conceptually?
SwiftUI’s diffing system determines what changed between two states of your UI so it can update only what’s necessary. Here’s the conceptual model:
1. Views are values, not objects
A View in SwiftUI is a lightweight, immutable struct describing what the UI should look like, not an actual on-screen object. Every time state changes, SwiftUI calls your body again and gets a brand new tree of view values. There’s no mutation of existing views — you get a whole new description each time.
2. The view tree vs. the render tree
SwiftUI maintains an internal, persistent tree (sometimes called the “identity tree” or render tree) that’s separate from the view structs you write. This is where actual state, animations, and layout data live. Your body produces a description; SwiftUI’s runtime reconciles that description against the persistent tree.
3. Diffing walks the tree structurally
When new view values come in, SwiftUI compares the new tree against the old one, node by node, in the same structural position:
- If a view’s type at a given position is the same as before, SwiftUI treats it as “the same view” and just updates its properties (a diff of the struct’s stored values) — no teardown/recreation.
- If the type differs (e.g., a
Textwhere there used to be anImage), SwiftUI tears down the old view (running.onDisappear, releasing state) and mounts a new one. - Container views (
VStack,ForEach, conditionals) recurse into their children and diff them positionally.
4. Identity is the key concept
This is the crux of it: SwiftUI needs to know whether view A in the new tree is view A from the old tree (just updated) or a different view that happens to occupy the same slot. Two kinds of identity matter:
- Structural identity — implicit identity based on a view’s type and its position/path in the view tree hierarchy. This is what powers plain
if/else, stacks, etc. - Explicit identity — via
.id()orIdentifiableconformance (used heavily inForEach). This lets SwiftUI track a view across reorderings, insertions, or deletions — critical for lists, since position alone isn’t a reliable signal when items move around.
5. Why this matters practically
- Wrapping a view in
AnyViewerases its static type, which can break structural diffing optimizations, since SwiftUI can no longer reason about the concrete type at compile time as easily. - Using
if/elseto swap between fundamentally different view types causes teardown/recreate (losing state), whereas modifying properties on the same view type preserves identity and state. - In
ForEach, giving items stable, correctids is what lets SwiftUI animate insertions/deletions/moves correctly instead of just diffing values by index.
6. Property-level diffing avoids unnecessary layout/render work
Once SwiftUI has decided “this is the same view, just updated,” it compares the stored properties (this is roughly why Equatable-like behavior matters) to decide whether layout and rendering actually need to happen again, or whether nothing observable changed.
So conceptually: SwiftUI reduces your declarative body into value-diffing over a persistent tree, using type + position (or explicit IDs) as the notion of “identity,” and only touches the parts of the render tree whose identity persists but whose values changed.