본문으로 건너뛰기
cccskills
무료GitHub에 공개

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md8.1 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

Generate accessibility infrastructure for VoiceOver, Dynamic Type, and accessibility features. Use when improving app accessibility, adding accessibility labels and hints, or auditing compliance.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

Generates an Apple-compliant account deletion flow with multi-step confirmation UI, optional data export, configurable grace period, Keychain cleanup, and server-side deletion request. Use when user needs account deletion, right-to-delete, or Apple App Review compliance for account removal.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

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 re-engagement campaigns, designing conversion values, or migrating from SKAdNetwork.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

alarmkit

무료

AlarmKit integration for scheduling alarms and timers with custom UI, Live Activities, and snooze support. Use when implementing alarm or timer features in iOS 26+ apps.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

Interpret app metrics and make data-driven decisions. Covers DAU/MAU, retention, LTV, ARPU, App Store Connect analytics, AARRR funnel analysis, cohort analysis, and diagnostic decision trees. Use when user wants to understand their metrics, diagnose problems, or build a data-driven growth plan.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rshankras/claude-code-apple-skills8122026년 7월 24일 업데이트

rshankras의 스킬 모두 보기

이 스킬의 문제 신고