skills/swiftui/data-flow/SKILL.md
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."
npx skillsauth add rshankras/claude-code-apple-skills data-flowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Structural identity = type + position in the hierarchy. An if/else creates two
identities (_ConditionalContent) — flipping the branch destroys one view and creates
another: 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 —
// ❌ 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.
@State/@StateObject storage
tears down and reinitializes. If state "randomly resets," find the identity change.@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).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 AnyView
forces 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.Image, not the whole
model). 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 only
when a property it actually read changes. Computed properties track through to the stored
properties they read. Works through 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 — every
parent body re-run reallocates it (heap churn + data loss). That's what @StateObject (or
@State + @Observable) exists for.@State for the same value desync — lift to the container,
hand children Bindings. One source of truth per fact.@SceneStorage (restoration state, per window) and @AppStorage (settings) are stores
next to your model, not the model. Limit total sources of truth..task { await … }; never assume when or how often body runs.Self._printChanges() (or expression Self._printChanges() at an
LLDB 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, worse performance) — use
@ViewBuilder helpers and switch instead; body already is a ViewBuilder.View is @MainActor: body, @State, members, and Task { } created in body are all
main-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.withAnimation
synchronously first, then start the async work; finish with another synchronous mutation.await can resume after the frame deadline — time-sensitive state (gesture/scroll
reactions) must mutate synchronously in the same frame. 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-practicesdevelopment
US web checkout via the StoreKit External Purchase Link entitlement — currently 0% Apple commission (litigation ongoing), how to ship it safely, and how to architect for a commission flip so a future ruling is a config change, not a rewrite. Use when adding external purchase links, weighing web checkout vs IAP, or planning US-storefront pricing strategy.
tools
Revenue beyond the single-app price tag — own-app bundles, Family Sharing as a conversion lever, cross-developer bundles & suites, and institutional licensing via Group Purchases / Apple School & Business Manager. Use when a developer has multiple apps, a subscription worth sharing, complementary indie partners, or school/clinic/business buyers.
testing
Run a structured accessibility audit on an iOS/macOS app — automated XCUITest audits, Accessibility Inspector, manual VoiceOver/Dynamic Type passes, and App Store Accessibility Nutrition Label evaluation. Use before release, when preparing Nutrition Label declarations, or for EU Accessibility Act compliance.
tools
Stage-by-stage audit of an app's App Store growth machinery against a 54-item P0–P9 playbook — every item scored from an App Store Connect MCP call, a codebase check, or an explicit question to the user, then routed to the skill or command that fixes it. Read-only on App Store Connect. Use for a growth audit or scorecard, a pre-launch growth plan, a quarterly re-audit, or "which growth levers am I missing."