Skip to content
Development
Skill

/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

From plugin
rshankras-apple-skills
848181 skills

Claude Code

$ npx -y skills add rshankras/claude-code-apple-skills --skill data-flow --agent claude-code

SKILL.md

data-flow.SKILL.md
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

*

Read more
@rshankras
Ships withrshankras-apple-skills

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.

Stars on GitHub

Momentum

stars in total
848
Star history of rshankras/claude-code-apple-skills on GitHub, a chart by star-history.com

Other skills on rshankras-apple-skills.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills10d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

Questions about data-flow.

Which coding agents can use data-flow?
data-flow installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.