/data-flow
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
$ npx -y skills add rshankras/claude-code-apple-skills --skill data-flow --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/data-flow
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
data-flow.SKILL.mdname: 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 —
// ❌ 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..<n`) only with a constant range.
- **Constant views per element**: an `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.
- List/Table gather all identifiers **eagerly** — cheap IDs = fast loads.
Dependencies: the graph, not the tree
- Every piece of data read in body is a dependency; only views whose dependency changed
re-run, and value comparison prunes unchanged subtrees. Stable identity is "the backbone of the dependency graph."
- **Scope dependencies tightly**: pass the subview what it renders (the `Image`, not the whole
model). Extracting subviews is free — "breaking up one view into multiple doesn't hurt performance" — and shrinks invalidation scope.
- **Observation** (`@Observable`) tracks **per property, per instance** — a view re-renders only
when a property it actually *read* changes, including through computed properties, arrays, optionals, and nesting.
- Migration from `ObservableObject`: drop conformance + `@Published` → `@Observable`;
`@ObservedObject` → delete or `@Bindable`; `@EnvironmentObject` → `@Environment`. Invalidation narrows from whole-object to read-properties — a free performance win.
State ownership: the decision rules
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 |
- ❌ Never allocate a reference-type model inline as an `@ObservedObject` default — every
re-run reallocates it (heap churn, data loss); use `@StateObject` or `@State` + `@Observable`.
- ❌ Two siblings each holding `@State` for the same value desync — lift state to the container
and hand children Bindings.
- `@SceneStorage` (restoration state, per window) and `@AppStorage` (settings) are stores
*
Read more
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 —
// ❌ 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..<n`) only with a constant range.
- **Constant views per element**: an `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.
- List/Table gather all identifiers **eagerly** — cheap IDs = fast loads.
Dependencies: the graph, not the tree
- Every piece of data read in body is a dependency; only views whose dependency changed
re-run, and value comparison prunes unchanged subtrees. Stable identity is "the backbone of the dependency graph."
- **Scope dependencies tightly**: pass the subview what it renders (the `Image`, not the whole
model). Extracting subviews is free — "breaking up one view into multiple doesn't hurt performance" — and shrinks invalidation scope.
- **Observation** (`@Observable`) tracks **per property, per instance** — a view re-renders only
when a property it actually *read* changes, including through computed properties, arrays, optionals, and nesting.
- Migration from `ObservableObject`: drop conformance + `@Published` → `@Observable`;
`@ObservedObject` → delete or `@Bindable`; `@EnvironmentObject` → `@Environment`. Invalidation narrows from whole-object to read-properties — a free performance win.
State ownership: the decision rules
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 |
- ❌ Never allocate a reference-type model inline as an `@ObservedObject` default — every
re-run reallocates it (heap churn, data loss); use `@StateObject` or `@State` + `@Observable`.
- ❌ Two siblings each holding `@State` for the same value desync — lift state to the container
and hand children Bindings.
- `@SceneStorage` (restoration state, per window) and `@AppStorage` (settings) are stores
*
A collection of Claude Code skills for iOS, macOS, watchOS, visionOS, and Apple platform development. These skills help you plan and build apps, maintain code quality, ensure HIG compliance, and guide you from idea to App Store.
Repo: rshankras/claude-code-apple-skills
Other skills on rshankras-apple-skills.
- /app-store
App Store optimization and marketing skills for descriptions, screenshots, keywords, review responses, and comprehensive promotional strategy. Use when user needs help with App Store presence, ASO, marketing, or customer communication.
Open skill - /ad-attribution
Privacy-preserving ad measurement with AdAttributionKit (SKAdNetwork's successor) — install and re-engagement attribution, conversion-value strategy under crowd anonymity, and end-to-end postback testing. Use when running paid acquisition beyond Apple Ads, measuring
Open skill - /app-description-writer
Generate compelling App Store descriptions that convert browsers into users. Use when writing initial descriptions, improving existing copy, or drafting promotional text and What's New for a major update.
Open skill - /apple-search-ads
Apple Search Ads campaign strategy for indie developers — paid acquisition, keyword bidding, budget planning, and ROAS optimization. Use when user asks about running ads, paid user acquisition, or Apple Search Ads campaigns.
Open skill - /iap-finalizer
Take a one-time in-app purchase from MISSING_METADATA to READY_TO_SUBMIT in App Store Connect — set its price schedule and localized display name/description (and optional review screenshot) via the ASC REST API. Use at Phase 6 (Pre-Release), after the IAP is built in-app (Phase
Open skill - /keyword-optimizer
Optimize app title, subtitle, and keywords for maximum App Store discoverability. Use when launching a new app, improving search rankings, entering new markets/languages, or safely optimizing ASO for an app with existing traffic.
Open skill

