このページは英語でのみご覧いただけます。
SwiftUI
Pathruleルール4件 • メモリ2件 • スキル1件
The modern Apple stack is narrow and opinionated: SwiftUI for views, the @Observable macro for state, SwiftData for persistence, NavigationStack for routing, and a compiler that now rejects data races instead of letting them crash at runtime. Assistants keep generating the previous generation of that stack, which either fails to build under Swift 6 or compiles into a view that reloads on every keystroke. This bundle fixes the state ownership, concurrency isolation, view body discipline, and SwiftData context rules that decide whether an app stays responsive.
推奨されるパスの配置
Pathruleは各要素を対応するパスに配置するため、アシスタントはそれが属する場所でだけ参照します。これはインポート時に適用されるスコープで、ワークスペースで調整できます。
ルール
4One owner per piece of state, expressed with the Observable macro/Features高強制View-local state uses State, shared state uses an Observable class on the MainActor, and no new code uses ObservableObject or published wrappers.
| 1 | SwiftUI redraws based on what a view actually reads. The Observation framework tracks that per property, which is why the old protocol-plus-wrapper approach is both more code and slower. |
| 2 | |
| 3 | - Mark shared state classes with `@Observable` and annotate them `@MainActor`. Views read their properties directly; there is no `@Published`, no `objectWillChange`, and no `@StateObject`. |
| 4 | - Own the instance where its lifetime belongs: `@State private var model = Model()` in the view that creates it, `@Environment` for something app-wide, and a plain `let` for a model passed in. Never create an observable model in a computed property or in `body`. |
| 5 | - Use `@Bindable` when a child needs two-way access to an observable's property, and `@Binding` for value types owned by a parent. |
| 6 | - Keep `@State` for genuinely view-local values (a text field's draft, a sheet flag). If two views need it, it belongs to a model, not to a parent's binding chain. |
| 7 | - Do not mirror the same value in two owners and sync them in `onChange`. Derive it with a computed property instead. |
Satisfy Swift 6 isolation instead of silencing it/App高強制UI types stay on the MainActor, shared mutable state lives in an actor, values crossing boundaries are Sendable, and unchecked Sendable is not a fix.
| 1 | Swift 6 turns data races into compile errors. Every warning it raises is a race the old code had and nobody noticed. |
| 2 | |
| 3 | - Annotate UI-facing types `@MainActor` (views already are) and let the compiler push background work outward, rather than sprinkling `DispatchQueue.main.async` at the call sites. |
| 4 | - Put shared mutable state in an `actor` and reach it with `await`. A class with a lock that you promise is safe is exactly what `@unchecked Sendable` hides; use it only for a type you have genuinely audited, and write down why. |
| 5 | - Make types that cross a boundary `Sendable`. Prefer value types and immutable stored properties, since that makes conformance automatic. |
| 6 | - Prefer structured concurrency: `async let` and `TaskGroup` for parallel work, `.task` for view-scoped work. An unstructured `Task { }` inherits no cancellation and no isolation guarantees, so reserve it for genuine fire-and-forget and store the handle if it must be cancellable. |
| 7 | - Never block a thread waiting for async work (no semaphores around `Task`), and never call a synchronous API that blocks on the main actor. |
Keep view bodies cheap and identities stable/Features中強制A body only describes UI: no I/O, no formatting work, no sorting; lists use stable ids and async work runs in task with cancellation.
| 1 | `body` can be evaluated many times per second. Anything expensive in it is multiplied by every render, and unstable identity throws away the view state SwiftUI was preserving. |
| 2 | |
| 3 | - Do no work in `body` beyond reading state and composing views. Precompute derived collections, formatted strings, and date math in the model (a computed property on an `@Observable` type is fine, a network call is not). |
| 4 | - Start async work with `.task { }` (or `.task(id:)` when it should restart on a value change), never `onAppear` plus an unstructured `Task`. `.task` is cancelled automatically when the view goes away, which is what prevents the classic "update after dismiss" crash. |
| 5 | - Give `ForEach` a genuinely stable identity: an `Identifiable` model or an explicit `id:` keyed to a real identifier. Using an array index reuses the wrong state when the collection reorders. |
| 6 | - Extract subviews so state changes redraw the smallest region that changed, and pass the narrowest data a subview needs rather than the whole model. |
| 7 | - Watch the modifier order (`frame` before `background`, `padding` before `border`), because in SwiftUI order is semantics, not style. |
Respect SwiftData context boundaries/Models高強制The UI reads through the main context, writes off the main thread go through a background context or ModelActor, and identifiers cross boundaries instead of models.
| 1 | SwiftData models are not `Sendable`. A model object belongs to the context that fetched it, and moving one across an isolation boundary is the source of the crashes people blame on SwiftData being immature. |
| 2 | |
| 3 | - Configure one `ModelContainer` at the app entry and inject it with `.modelContainer(...)`. Views fetch with `@Query` and mutate through `@Environment(\.modelContext)`. |
| 4 | - Do bulk or long-running work in a background context (a `ModelActor`, or a context created on the actor doing the work) and save there. Do not hand a fetched model to a `Task` on another actor. |
| 5 | - Pass a `PersistentIdentifier` across boundaries and re-fetch on the other side. That is the only safe way to refer to the same row from another context. |
| 6 | - Keep `@Query` predicates and sort descriptors in the query itself so the store does the filtering. Fetching everything and filtering in Swift defeats the index and loads the whole table. |
| 7 | - Model relationships explicitly with `@Relationship`, choose the delete rule deliberately, and mark unique constraints; SwiftData will not guess your integrity rules. |
| 8 | |
| 9 | See /App for the concurrency rule this depends on. |
メモリ
2スキル
1swiftui-concurrency-review/rootPre-merge checklist for SwiftUI changes under Swift 6 strict concurrency and SwiftData.
| 1 | --- |
| 2 | name: swiftui-concurrency-review |
| 3 | description: Review checklist for SwiftUI code under Swift 6 strict concurrency: state ownership, actor isolation, view body cost, SwiftData context boundaries, and navigation state. Run before merging. |
| 4 | --- |
| 5 | |
| 6 | # SwiftUI review |
| 7 | |
| 8 | ## State |
| 9 | - [ ] Shared state is an `@Observable` `@MainActor` class; no new `ObservableObject` or `@Published`. |
| 10 | - [ ] Each value has exactly one owner; no mirrored copies synced in `onChange`. |
| 11 | - [ ] Models are not constructed inside `body` or a computed view property. |
| 12 | |
| 13 | ## Concurrency |
| 14 | - [ ] No new `@unchecked Sendable`; shared mutable state is in an actor. |
| 15 | - [ ] Async work uses `.task` (or `.task(id:)`) and is cancellable; no detached `Task` for view work. |
| 16 | - [ ] Nothing blocks a thread waiting on async work; no semaphores around `Task`. |
| 17 | - [ ] The build is clean under strict concurrency, with no suppressed warnings. |
| 18 | |
| 19 | ## Views |
| 20 | - [ ] `body` does no I/O, formatting, or sorting. |
| 21 | - [ ] `ForEach` uses a stable identifier, not an array index. |
| 22 | - [ ] Subviews are extracted so a change redraws the smallest region. |
| 23 | |
| 24 | ## SwiftData |
| 25 | - [ ] Models never cross an isolation boundary; `PersistentIdentifier` does. |
| 26 | - [ ] Bulk writes run in a background context or `ModelActor`. |
| 27 | - [ ] `@Query` filters and sorts in the query, not in Swift. |
| 28 | |
| 29 | ## Navigation |
| 30 | - [ ] Routing is a typed path array owned by a model; no `NavigationView`. |
| 31 | - [ ] Deep link and restoration go through the same path decode. |
このパターンを使う理由
AI agents write SwiftUI with ObservableObject and published properties, do work inside view bodies, and cross actor boundaries in ways Swift 6 strict concurrency rejects.
対象:iOS and macOS teams on Swift 6 with SwiftUI and SwiftData
アシスタントが次のことをしないようにします。
- Generating ObservableObject and published wrappers where the Observable macro is the current API
- Doing network or formatting work inside a view body so it reruns on every render
- Passing a SwiftData model between actors instead of its persistent identifier
- Silencing a concurrency error with unchecked Sendable instead of fixing the isolation
- ライセンス
- Apache-2.0
- バージョン
- 1.0.0
- 更新日
- 2026-08-24