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
603183 skills
Install
$ npx -y skills add rshankras/claude-code-apple-skills --skill data-flow --agent claude-code

How 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.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
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.

Get the whole plugin
Stats
603
Stars
51
Forks
Active
Maintenance
Swift
Language
MIT
License
16d ago
Last commit
9mo ago
Created

Repo: rshankras/claude-code-apple-skills

Other skills on rshankras-apple-skills.