# Pathrule Pattern: SwiftUI (1.0.0)
# ::pathrule:package:swiftui

### [RULE] One owner per piece of state, expressed with the Observable macro  (path: /Features)
<!-- scope: folder | priority: high | strict -->

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.

- Mark shared state classes with `@Observable` and annotate them `@MainActor`. Views read their properties directly; there is no `@Published`, no `objectWillChange`, and no `@StateObject`.
- 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`.
- Use `@Bindable` when a child needs two-way access to an observable's property, and `@Binding` for value types owned by a parent.
- 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.
- Do not mirror the same value in two owners and sync them in `onChange`. Derive it with a computed property instead.

---

### [RULE] Satisfy Swift 6 isolation instead of silencing it  (path: /App)
<!-- scope: folder | priority: high | strict -->

Swift 6 turns data races into compile errors. Every warning it raises is a race the old code had and nobody noticed.

- 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.
- 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.
- Make types that cross a boundary `Sendable`. Prefer value types and immutable stored properties, since that makes conformance automatic.
- 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.
- Never block a thread waiting for async work (no semaphores around `Task`), and never call a synchronous API that blocks on the main actor.

---

### [RULE] Keep view bodies cheap and identities stable  (path: /Features)
<!-- scope: folder | priority: medium | strict -->

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

- 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).
- 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.
- 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.
- Extract subviews so state changes redraw the smallest region that changed, and pass the narrowest data a subview needs rather than the whole model.
- Watch the modifier order (`frame` before `background`, `padding` before `border`), because in SwiftUI order is semantics, not style.

---

### [RULE] Respect SwiftData context boundaries  (path: /Models)
<!-- scope: folder | priority: high | strict -->

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.

- Configure one `ModelContainer` at the app entry and inject it with `.modelContainer(...)`. Views fetch with `@Query` and mutate through `@Environment(\.modelContext)`.
- 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.
- 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.
- 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.
- Model relationships explicitly with `@Relationship`, choose the delete rule deliberately, and mark unique constraints; SwiftData will not guess your integrity rules.

See /App for the concurrency rule this depends on.

---

### [MEMORY] Navigation is data: NavigationStack with a typed path  (path: /Features)

`NavigationStack` replaced the deprecated `NavigationView`, and the point of the replacement is that navigation became state you can inspect.

- Drive the stack with `NavigationStack(path: $router.path)` where `path` is an array of route values, and register destinations with `navigationDestination(for:)`. Pushing is appending; popping is removing; going home is emptying the array.
- Make the route type a small `Hashable` enum or a value struct, not a view. A route carries an id, not a rendered screen.
- Own the path in an `@Observable` router when more than one screen needs to navigate, and keep it `@MainActor` like the rest of the UI state.
- Deep links and state restoration become a decode into that array, which is why this design is worth it: one code path for a tap, a notification, and a universal link.
- Use `NavigationSplitView` for multi-column layouts on iPad and Mac rather than reimplementing a sidebar, and let it collapse on compact widths.

See /Features for the state ownership rule the router follows.

---

### [MEMORY] Dependencies, previews, and tests without a hidden singleton  (path: /App)

The main reason SwiftUI code becomes untestable is a view or model that reaches for a shared singleton. Injecting instead costs nothing and pays for itself in previews.

- Define a protocol for anything that leaves the process (network, keychain, notifications, analytics), inject it into the observable model, and default to the live implementation at the app entry.
- Expose app-wide dependencies through a custom `EnvironmentKey` (or `@Entry`) so a view subtree can be re-pointed at a fake. Reading a global `.shared` inside a view makes every preview a live request.
- Give each screen a `#Preview` with a fake model in a fixed state, including the loading and error states. A preview that only shows the happy path is the reason nobody notices the empty state is broken.
- Write tests with Swift Testing (`@Test`, `#expect`, `@Suite`) for new code, and keep XCTest for UI tests and existing suites. Both can coexist in a project.
- Test the model, not the view body: given a fake service, assert the state the view will render. Snapshot or UI tests cover layout, not logic.

See /Features for state ownership and /Models for the SwiftData boundary these fakes stand in for.

---

### [SKILL] swiftui-concurrency-review  (path: /)

---
name: swiftui-concurrency-review
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.
---

# SwiftUI review

## State
- [ ] Shared state is an `@Observable` `@MainActor` class; no new `ObservableObject` or `@Published`.
- [ ] Each value has exactly one owner; no mirrored copies synced in `onChange`.
- [ ] Models are not constructed inside `body` or a computed view property.

## Concurrency
- [ ] No new `@unchecked Sendable`; shared mutable state is in an actor.
- [ ] Async work uses `.task` (or `.task(id:)`) and is cancellable; no detached `Task` for view work.
- [ ] Nothing blocks a thread waiting on async work; no semaphores around `Task`.
- [ ] The build is clean under strict concurrency, with no suppressed warnings.

## Views
- [ ] `body` does no I/O, formatting, or sorting.
- [ ] `ForEach` uses a stable identifier, not an array index.
- [ ] Subviews are extracted so a change redraws the smallest region.

## SwiftData
- [ ] Models never cross an isolation boundary; `PersistentIdentifier` does.
- [ ] Bulk writes run in a background context or `ModelActor`.
- [ ] `@Query` filters and sorts in the query, not in Swift.

## Navigation
- [ ] Routing is a typed path array owned by a model; no `NavigationView`.
- [ ] Deep link and restoration go through the same path decode.
