--- name: data-flow description: SwiftUI's actual mental model — view identity, lifetime, and dependencies (the Demystify canon), state ownership decision rules, Observation's per-property tracking, body-performance discipline, and the main-actor concurrency contract. Use when state resets mysteriously, views re-render too often, animations glitch between branches, choosing @State vs @Bindable vs plain property, or debugging "why did body run." allowed-tools: [Read, Write, Edit, Glob, Grep] last_verified: 2026-07-16 review_by: 2027-06-22 os_version: iOS 27 / macOS 27 --- # SwiftUI Data Flow Nearly every confusing SwiftUI bug — state that resets, animations that crossfade instead of move, lists that flash, bodies that run constantly — traces to identity, lifetime, or dependencies. This is Apple's own mental model (the Demystify sessions + Data Essentials + Observation), current through the WWDC26 `@State` macro. ## When This Skill Activates - "@State resets when…" / state loses its value on a condition change - Views re-render too often; animations crossfade when they should move - Lists flashing, rows reordering wrongly, `ForEach` misbehaving - Choosing between `@State`, `@Binding`, `@Bindable`, `@Environment`, plain property - Debugging with `Self._printChanges()`; concurrency warnings in view code ## Identity: the root concept SwiftUI sees three things: **identity, lifetime, dependencies**. Views with the same identity are "different states of the same conceptual UI element"; distinct identities are distinct views. - **Structural identity** = type + position in the hierarchy. An `if/else` creates **two identities** (`_ConditionalContent`) — flipping the branch destroys/recreates the view: state resets, transitions crossfade instead of animating. - **Explicit identity** = `id:` in ForEach or `.id(_:)` (also the target for `ScrollViewReader.scrollTo`). Changing an explicit id is a new identity — new lifetime, fresh state. (That's the `.id(item.id)` force-refresh trick — use it knowingly.) - **The inert-modifier rule** (the most under-used fix): prefer one view whose modifiers vary over branching — ```swift // ❌ two identities; state resets, transition crossfades if expired { content.opacity(0.3) } else { content } // ✅ one identity; cheap, pruned when inert content.opacity(expired ? 0.3 : 1.0) ``` Inert values (opacity 1, padding 0) cost nothing. "By default, try to preserve identity." - Conditionally include a view *inside* a stack rather than conditionally wrapping the stack. ## Lifetime: state is tied to identity - View **values** are ephemeral — created for comparison, then destroyed. Never rely on the struct instance; identity provides continuity. - "Whenever the identity changes, the state is replaced" — `@State`/`@StateObject` storage tears down and reinitializes. If state "randomly resets," find the identity change. - WWDC26: `@State` is a macro with **lazy initialization of `@Observable` classes** (backported to iOS 17) — the stored object initializes once per lifetime, not on every view-value init. Remove default values when also assigning in `init` (source-breaking edge). ## ForEach identifier rules (the flashing-list checklist) - **Stable** — never `var id = UUID()` computed per access (everything flashes/reanimates). - **Not indices** — insert-at-front reads as insert-at-end; rows animate wrongly. - **Unique** — duplicate IDs drop rows. - Use persistent/database-derived IDs; that's what `Identifiable` is for. Range ForEach (`0..