Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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."
.claude/skills/rshankras-data-flow/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 123% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 35% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 108% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 104% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 99% | 0% |
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.
ForEach misbehaving@State, @Binding, @Bindable, @Environment, plain propertySelf._printChanges(); concurrency warnings in view codeSwiftUI 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.
if/else creates twoidentities (_ConditionalContent) — flipping the branch destroys/recreates the view: state resets, transitions crossfade instead of animating.
id: in ForEach or .id(_:) (also the target forScrollViewReader.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.)
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."
struct instance; identity provides continuity.
@State/@StateObject storagetears down and reinitializes. If state "randomly resets," find the identity change.
@State is a macro with lazy initialization of @Observable classes (backportedto 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).
var id = UUID() computed per access (everything flashes/reanimates).Identifiable is for. Range ForEach(0..<n) only with a constant range.
if filter inside ForEach (0-or-1 views) or AnyViewforces List to resolve every row just to count them. Filter in the data, and cache the filtered collection in the model — an inline .filter re-runs linearly on every body.
re-run, and value comparison prunes unchanged subtrees. Stable identity is "the backbone of the dependency graph."
Image, not the wholemodel). Extracting subviews is free — "breaking up one view into multiple doesn't hurt performance" — and shrinks invalidation scope.
@Observable) tracks per property, per instance — a view re-renders onlywhen a property it actually read changes, including through computed properties, arrays, optionals, and nesting.
ObservableObject: drop conformance + @Published → @Observable;@ObservedObject → delete or @Bindable; @EnvironmentObject → @Environment. Invalidation narrows from whole-object to read-properties — a free performance win.
Ask Apple's three questions: what data does the view need · how does it manipulate it · where does truth live?
| Situation | Use | |---|---| | Display-only, parent owns it | plain let property | | Transient, view-local UI state | @State (group related fields into one struct with mutating methods) | | Write access to someone else's truth | @Binding (bindings compose: $config.note) | | Observable model owned by this view | @State (lazy-init since WWDC26 macro) | | Observable model, needs $model.field bindings only | @Bindable | | Observable model, globally available | @Environment | | Observable model, none of the above | plain property |
@ObservedObject default — everyre-run reallocates it (heap churn, data loss); use @StateObject or @State + @Observable.
@State for the same value desync — lift state to the containerand hand children Bindings.
@SceneStorage (restoration state, per window) and @AppStorage (settings) are storesnext to your model, not the model. Limit total sources of truth.
string-building; move loading to .task { await … }.
Self._printChanges() (or expression Self._printChanges() at anLLDB breakpoint): @self = view value changed; a named property = that dependency changed. Debug-only — never ship it. Deeper workflow: performance/swiftui-debugging.
AnyView hides structure from SwiftUI (worse diagnostics/performance) — use@ViewBuilder helpers and switch instead.
View is @MainActor: body, @State, members, and Task { } created in body are allmain-actor — most view code needs zero annotations (and Swift 6.2's default-isolation mode removes the rest).
Shape.path(in:), Layout methods,visualEffect, onGeometryChange — that's why they're Sendable. Don't touch self.someState there; copy the value in the capture list ([pulse]) and compute from the proxies SwiftUI hands you.
await can resume after the frame deadline, so time-sensitive state (gesture/scrollreactions, button loading indicators) must mutate synchronously before starting async work. Bridge UI↔async through a piece of state; keep view Tasks minimal ("inform the model") so async logic stays unit-testable.
Data-flow review: Symptom | Root cause (identity / lifetime / dependency / ownership) | Fix — check identity first; it explains most of the rest.
performance/swiftui-debugging (Instruments workflow), swiftui/layout, swift/concurrency-patterns, ios/coding-best-practicesOther measured skills in the registry, with their headline benchmark lift.