agentsclimarketplace

Data flow

Skill rshankras/claude-code-apple-skills/skills/swiftui/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 re-render too often, animations glitch between branches, choosing @State vs @Bindable vs plain property, or debugging "why did body run."From its SKILL.md

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

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

8.1 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

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?

SituationUse
Display-only, parent owns itplain 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 aboveplain 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 next to your model, not the model. Limit total sources of truth.

Body discipline

  • Body must be a pure function, free of side effects — no allocation, I/O, filtering, or string-building; move loading to .task { await … }.
  • Debug why body ran with 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/performance) — use @ViewBuilder helpers and switch instead.

Concurrency contract (WWDC25)

  • 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).
  • SwiftUI runs some of your closures off-main — 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.
  • Every await can resume after the frame deadline, so time-sensitive state (gesture/scroll reactions, 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.

Output Format

Data-flow review: Symptom | Root cause (identity / lifetime / dependency / ownership) | Fix — check identity first; it explains most of the rest.

References

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.