Approach: Production-First Iterative Refactoring — This skill is built for production enterprise codebases where stability and reviewability matter more than speed. Architecture changes are delivered through iterative refactoring — small, focused PRs (≤200 lines, single concern) tracked in a refactoring/ directory. Critical safety issues ship first; cosmetic improvements come last.
SwiftUI MVVM Architecture (iOS 17+)
Enterprise-grade SwiftUI MVVM architecture skill. Opinionated: prescribes @Observable ViewModels, Router navigation, constructor injection, ViewState enum, and Repository-based networking. Adopts a production-first iterative refactoring approach — every pattern is chosen for testability, reviewability, and safe incremental adoption in large teams. For non-architectural SwiftUI API guidance (animations, modern API replacements, Liquid Glass), use a general SwiftUI skill instead.
Architecture Layers
View Layer → SwiftUI Views. Declarative UI only. Owns ViewModel via @State.
ViewModel Layer → @Observable @MainActor final class. Exposes ViewState<T>.
Repository Layer → Protocol-based data access. Hides data source details.
Service Layer → URLSession, persistence. Injected via protocol.
Quick Decision Trees
"Should this View have a ViewModel?"
Is there business logic, networking, or complex state?
├── YES → Create @Observable ViewModel
└── NO → Is it a reusable UI component (button, card, cell)?
├── YES → Plain struct with data parameters, NO ViewModel
└── NO → No ViewModel needed unless it simplifies testing
"How should I own this ViewModel?"
Does THIS view create the ViewModel?
├── YES → @State private var viewModel = MyViewModel()
└── NO → Does the view need $ bindings to ViewModel properties?
├── YES → @Bindable var viewModel: MyViewModel
└── NO → let viewModel: MyViewModel (plain property)
"Where do dependencies come from?"
ViewModel always receives dependencies via constructor:
init(repository: ItemRepositoryProtocol)
How does the View get the dependency to pass?
├── Shared service (used across many screens)
│ └── Register via @Entry in EnvironmentValues
│ View reads @Environment(\.repo), passes to VM init
├── Screen-specific dependency (passed by parent)
│ └── View receives it as init parameter, passes to VM init
└── Outside view hierarchy (background service, deep utility)
└── @Injected property wrapper (legacy/convenience only)
Workflows
Default workflow: Analyze & Refactor (below). New screen creation applies the same patterns but from a clean slate. In production enterprise codebases, most work is iterative modernization — not greenfield.
Workflow: Analyze & Refactor Existing Codebase
When: First encounter with a legacy SwiftUI codebase — the most common enterprise scenario.
- Scan for anti-patterns using the detection checklist (
references/anti-patterns.md)
- Create
refactoring/ directory with per-feature plan files (references/refactoring-workflow.md)
- Write each issue with full description (Location, Severity, Problem, Fix) — titles alone get forgotten
- Categorize issues by severity: 🔴 Critical → 🟡 High → 🟢 Medium
- Plan Phase 1 PR: fix critical safety issues only (≤200 lines per PR)
- Execute one PR at a time. New findings go to
refactoring/discovered.md with full descriptions, NOT into current PR
- After completing each fix: mark the task
- [x] in the feature file and update refactoring/README.md progress table
- Proceed through phases: Critical → @Observable migration → ViewState → Architecture
Workflow: Create a New Screen
When: Building a new feature screen from scratch. Apply enterprise patterns from the start so no refactoring is needed later.
- Define the data model and repository protocol (
references/networking.md)
- Create ViewModel:
@Observable @MainActor final class with ViewState<T> (references/mvvm-observable.md)
- Add
// MARK: - sections: Properties, Init, Actions, Computed Properties
- Create the screen View with
@State private var viewModel
- Wire data loading via
.task { await viewModel.load() }
- Add navigation route to Router enum (
references/navigation.md)
- Register dependencies in
@Environment or @Injected (references/dependency-injection.md)
- Create test file with mock repository (
references/testing.md)
Workflow: Migrate ViewModel from ObservableObject
When: Modernizing existing code from Combine-based observation to @Observable.
- Add
Self._printChanges() to the View body — note current redraw triggers
- Replace
ObservableObject conformance with @Observable macro
- Remove all
@Published — plain var properties are auto-tracked
- Replace
@StateObject with @State in the owning View
- Replace
@ObservedObject with plain let (or @Bindable if $ bindings needed)
- Replace
@EnvironmentObject with @Environment(Type.self)
- Add
@MainActor to the ViewModel class declaration
- Verify with
Self._printChanges() — confirm fewer/more specific redraw triggers
- Run existing tests — all must pass
- Remove
Self._printChanges() before committing
Code Generation Rules
- Mark ViewModels as
@Observable @MainActor final class
- Use
private(set) var for state properties modified only by the ViewModel
- Use
ViewState<T> enum for async data — never separate boolean flags
- Inject dependencies via constructor with protocol types
- Use
.task { } for initial data loading
- Keep View bodies pure — no
Task { } inside body, no business logic
- Use typed
enum Route: Hashable for navigation
- Add
// MARK: - sections: Properties, Init, Actions, Computed Properties
- Import only
Foundation (and domain modules) in ViewModels — never SwiftUI
- Keep every generated file ≤ 400 lines. Extract subviews into dedicated files. Split ViewModel logic into extensions (
MyVM+Search.swift) or child ViewModels when approaching that limit. For legacy files, log a split task in the feature's refactoring/ plan instead of forcing it mid-refactor.
- Before modifying a View or ViewModel, output a brief
<thought> analyzing its current state and redraw triggers.
@Observable instances passed to .environment(...) must be owned by a stable @State — .environment(AppTheme()) creates a new AppTheme on every parent redraw, which defeats the point of environment injection and breaks observation. Always own it: @State private var theme = AppTheme() then .environment(theme).
- ViewModel signals navigation intent via closures; View owns the
@State toggle. Instead of viewModel.router.push(.profile(user)), the ViewModel exposes var onOpenProfile: ((User) -> Void)? and the View sets .onOpenProfile = { user in path.append(.profile(user)) } at wire-up time. This keeps the ViewModel ignorant of navigation mechanics (testable, reusable across hosts) while the View remains the single owner of @State path.
Self._printChanges() verification steps for splitting views. When fixing redraw storms, verification is a sequence, not a single check: (1) add let _ = Self._printChanges() to both the parent view body AND the split child, (2) type/tap to produce change events, (3) confirm ONLY the target child prints during the interaction — if the parent also prints, it has a hidden read of the changing value (search the body for every store.property access and verify equality-only reads use stored, not computed, state), (4) remove both _printChanges before committing.
Anti-Pattern Severity Reference
When auditing a SwiftUI codebase, assign severities from this canonical table. Models systematically mis-classify these — memorize the boundaries.
| Anti-pattern |
Severity |
Why this level |
import SwiftUI in a ViewModel |
🔴 Critical |
ViewModel leaks View-layer types; makes the ViewModel untestable outside a SwiftUI host |
UIViewController/UIView reference inside a ViewModel |
🔴 Critical |
Same — hard coupling to UIKit, blocks cross-platform and blocks unit testing |
Missing @MainActor on a ViewModel that mutates UI-facing state |
🔴 Critical |
Undefined behavior — UI reads off-main-thread state, potential crashes |
Force-unwrap ! on an async result in a ViewModel |
🔴 Critical |
Crash vector in production |
viewModel declared as plain var without @State in the owning View |
🟡 High |
A new ViewModel instance is created on every parent redraw — loses all state, fires effects repeatedly |
.onAppear { Task { await viewModel.load() } } |
🟡 High |
Unmanaged: fires on every view appearance (back-nav, tab switch), not tied to task lifetime. Replace with .task { } |
| Business logic in View body (network calls, mutation) |
🟡 High |
Breaks reviewability and testability, but not a crash |
Separate isLoading / error / data boolean flags on a ViewModel |
🟢 Medium |
Functionally works but creates impossible states (isLoading && error != nil) — migrate to ViewState<T> enum |
Missing // MARK: - section comments |
🟢 Medium |
Cosmetic, affects reviewability |
@StateObject / @ObservedObject / @EnvironmentObject in an @Observable migration path |
🟢 Medium |
Works with legacy ObservableObject but should be migrated in phased PRs |
Decision rules:
- Critical = crash, data corruption, or blocks all testing
- High = not a crash, but breaks an architectural invariant that cascades (unmanaged tasks, lost state, hidden coupling)
- Medium = functional code with a long-tail maintenance cost or impossible states
Migration Mapping: ObservableObject → @Observable
This table must appear verbatim in every "migrate from ObservableObject" response — readers copy it into code reviews.
Legacy (ObservableObject) |
Modern (@Observable) |
Note |
class VM: ObservableObject |
@Observable class VM |
Add @MainActor final if it mutates UI state |
@Published var count |
var count |
Plain var — the macro auto-tracks reads |
@StateObject var vm = VM() in the owner |
@State private var vm = VM() |
@State owns the lifecycle; same semantics |
@ObservedObject var vm: VM in a child that only reads |
let vm: VM |
No wrapper — plain reference, tracked automatically |
@ObservedObject var vm: VM in a child that needs $vm.property |
@Bindable var vm: VM |
@Bindable enables two-way bindings without ownership |
@EnvironmentObject var theme: AppTheme |
@Environment(AppTheme.self) private var theme |
Type-based lookup; env value must be registered via .environment(themeInstance) |
ObservableObject + @Published tests assert on publisher changes |
Reads to properties inside withObservationTracking { } |
Modern Observation framework replaces Combine plumbing |
Common gotcha: models often apply @State to the VM in the owning view but forget to switch @ObservedObject → let or @Bindable in child views — the whole chain must migrate together or child views won't observe changes.
When generating tests, ALWAYS:
- Use protocol mocks with
var stubbed* and var *CallCount tracking
- Test through public interface, never test private methods
- Mark test classes/structs
@MainActor when testing @MainActor ViewModels
- Use
await fulfillment(of:) for async tests — NEVER wait(for:) (deadlocks)
- Include memory leak detection with
addTeardownBlock { [weak sut] in XCTAssertNil(sut) }
Fallback Strategies & Loop Breakers
- @State vs @Bindable Generics: If the compiler complains about property wrapper bindings (
$), ensure you use @Bindable in subviews for @Observable types. Why: @State creates ownership (single source of truth), while @Bindable enables two-way bindings without ownership — the compiler enforces this distinction. If unresolved, temporarily use plain let and closure callbacks to unblock compilation.
- NavigationStack Path Issues: If the compiler complains about
Hashable routes or navigationDestination types, ensure your enum Route is perfectly Hashable and avoid passing complex models (prefer passing IDs). Why: NavigationStack serializes the path for state restoration, so every route case must be deterministically hashable.
- Revert and Restart: If a View refactor spirals into 50+ compiler errors related to ambiguous type inference, stop. Propose reverting the changes and breaking the problem into two smaller phases (e.g. migrate properties first, then extract subviews). Why: SwiftUI's type inference cascades — a single change can destabilize unrelated code, and small PRs are far easier to review and debug.
Confidence Checks
Before finalizing generated or refactored code, verify ALL:
□ No duplicate functionality — searched codebase for existing implementations
□ Architecture adherence — follows patterns already established in the project
□ Naming conventions — matches existing project naming style
□ Import check — ViewModel imports only Foundation, NOT SwiftUI
□ @MainActor — present on all ViewModel class declarations
□ ViewState — used for all async data, no separate isLoading/error booleans
□ DI — dependencies injected via protocol, not accessed via singletons
□ Task management — .task modifier for lifecycle, explicit cancellation handling
□ CancellationError — handled silently, never shown to user
□ Tests — corresponding test file exists or is created alongside
□ PR scope — changes within defined scope, new findings go to `refactoring/discovered.md`
□ File size — new files ≤ 400 lines; existing oversized files have a split task logged in `refactoring/`
Companion Skills
Before generating async ViewModel, Task, or actor code: determine the project's concurrency approach. If unclear from context, ask the user.
| Project's concurrency stack |
Companion skill |
Apply when |
async/await, actors, Swift 6, @MainActor |
skills/swift-concurrency/SKILL.md |
Writing async ViewModel methods, Task creation, actor-isolated state |
DispatchQueue, OperationQueue (legacy or hybrid) |
skills/gcd-operations/SKILL.md |
Writing queue-based networking, background work, thread-safe state |
If unclear, ask: "Does this project use Swift Concurrency (async/await) or GCD for async operations?"
References
| Reference |
When to Read |
references/rules.md |
Do's and Don'ts quick reference: priority rules and critical anti-patterns |
references/mvvm-observable.md |
Creating ViewModels, @State/@Bindable ownership rules, migration mapping |
references/navigation.md |
Router pattern, deep linking, TabView setup, sheets |
references/dependency-injection.md |
@Environment, @Injected wrapper, constructor injection, testing DI |
references/networking.md |
ViewState enum, Repository pattern, HTTPClient, task cancellation |
references/anti-patterns.md |
Code review detection checklist, severity-ranked violations |
references/testing.md |
ViewModel unit tests, async patterns, mocks, memory leak detection |
references/performance.md |
Self._printChanges(), Instruments, launch time, verification evidence |
references/file-organization.md |
File size guidelines, extension splitting, child ViewModels, subview extraction |
references/refactoring-workflow.md |
refactoring/ directory protocol, per-feature plans, PR sizing, phase ordering |
1---2name: swiftui-mvvm3description: Use this skill when working with SwiftUI ViewModels — creating, refactoring, or testing them. Triggers for: setting up a ViewModel for a SwiftUI screen, extracting logic from a View into a ViewModel, migrating from ObservableObject to @Observable, modeling async state (instead of separate Bool flags like isLoading/hasError), injecting dependencies into ViewModels, writing unit tests for @Observable ViewModels, NavigationStack/Router setup, or any question about SwiftUI app architecture. Also use when a SwiftUI View imports too much business logic, when someone asks how to structure a SwiftUI screen 'the modern way,' or when they ask about @State/@Bindable ownership, ViewState patterns, or why their ViewModel shouldn't import SwiftUI.4---56> **Approach: Production-First Iterative Refactoring** — This skill is built for production enterprise codebases where stability and reviewability matter more than speed. Architecture changes are delivered through iterative refactoring — small, focused PRs (≤200 lines, single concern) tracked in a `refactoring/` directory. Critical safety issues ship first; cosmetic improvements come last.78# SwiftUI MVVM Architecture (iOS 17+)910Enterprise-grade SwiftUI MVVM architecture skill. Opinionated: prescribes @Observable ViewModels, Router navigation, constructor injection, ViewState enum, and Repository-based networking. Adopts a **production-first iterative refactoring** approach — every pattern is chosen for testability, reviewability, and safe incremental adoption in large teams. For non-architectural SwiftUI API guidance (animations, modern API replacements, Liquid Glass), use a general SwiftUI skill instead.1112## Architecture Layers1314```15View Layer → SwiftUI Views. Declarative UI only. Owns ViewModel via @State.16ViewModel Layer → @Observable @MainActor final class. Exposes ViewState<T>.17Repository Layer → Protocol-based data access. Hides data source details.18Service Layer → URLSession, persistence. Injected via protocol.19```2021## Quick Decision Trees2223### "Should this View have a ViewModel?"2425```26Is there business logic, networking, or complex state?27├── YES → Create @Observable ViewModel28└── NO → Is it a reusable UI component (button, card, cell)?29 ├── YES → Plain struct with data parameters, NO ViewModel30 └── NO → No ViewModel needed unless it simplifies testing31```3233### "How should I own this ViewModel?"3435```36Does THIS view create the ViewModel?37├── YES → @State private var viewModel = MyViewModel()38└── NO → Does the view need $ bindings to ViewModel properties?39 ├── YES → @Bindable var viewModel: MyViewModel40 └── NO → let viewModel: MyViewModel (plain property)41```4243### "Where do dependencies come from?"4445```46ViewModel always receives dependencies via constructor:47 init(repository: ItemRepositoryProtocol)4849How does the View get the dependency to pass?50├── Shared service (used across many screens)51│ └── Register via @Entry in EnvironmentValues52│ View reads @Environment(\.repo), passes to VM init53├── Screen-specific dependency (passed by parent)54│ └── View receives it as init parameter, passes to VM init55└── Outside view hierarchy (background service, deep utility)56 └── @Injected property wrapper (legacy/convenience only)57```5859## Workflows6061> **Default workflow**: Analyze & Refactor (below). New screen creation applies the same patterns but from a clean slate. In production enterprise codebases, most work is iterative modernization — not greenfield.6263### Workflow: Analyze & Refactor Existing Codebase6465**When:** First encounter with a legacy SwiftUI codebase — the most common enterprise scenario.66671. Scan for anti-patterns using the detection checklist (`references/anti-patterns.md`)682. Create `refactoring/` directory with per-feature plan files (`references/refactoring-workflow.md`)693. Write each issue with **full description** (Location, Severity, Problem, Fix) — titles alone get forgotten704. Categorize issues by severity: 🔴 Critical → 🟡 High → 🟢 Medium715. Plan Phase 1 PR: fix critical safety issues only (≤200 lines per PR)726. Execute one PR at a time. New findings go to `refactoring/discovered.md` with full descriptions, NOT into current PR737. After completing each fix: mark the task `- [x]` in the feature file and update `refactoring/README.md` progress table748. Proceed through phases: Critical → @Observable migration → ViewState → Architecture7576### Workflow: Create a New Screen7778**When:** Building a new feature screen from scratch. Apply enterprise patterns from the start so no refactoring is needed later.79801. Define the data model and repository protocol (`references/networking.md`)812. Create ViewModel: `@Observable @MainActor final class` with `ViewState<T>` (`references/mvvm-observable.md`)823. Add `// MARK: -` sections: Properties, Init, Actions, Computed Properties834. Create the screen View with `@State private var viewModel`845. Wire data loading via `.task { await viewModel.load() }`856. Add navigation route to Router enum (`references/navigation.md`)867. Register dependencies in `@Environment` or `@Injected` (`references/dependency-injection.md`)878. Create test file with mock repository (`references/testing.md`)8889### Workflow: Migrate ViewModel from ObservableObject9091**When:** Modernizing existing code from Combine-based observation to @Observable.92931. Add `Self._printChanges()` to the View body — note current redraw triggers942. Replace `ObservableObject` conformance with `@Observable` macro953. Remove all `@Published` — plain `var` properties are auto-tracked964. Replace `@StateObject` with `@State` in the owning View975. Replace `@ObservedObject` with plain `let` (or `@Bindable` if `$` bindings needed)986. Replace `@EnvironmentObject` with `@Environment(Type.self)`997. Add `@MainActor` to the ViewModel class declaration1008. Verify with `Self._printChanges()` — confirm fewer/more specific redraw triggers1019. Run existing tests — all must pass10210. Remove `Self._printChanges()` before committing103104## Code Generation Rules105106<critical_rules>107Whether generating new code or refactoring existing code, every output must be **production-ready and PR-shippable** — small, focused, and testable. ALWAYS:1081091. Mark ViewModels as `@Observable @MainActor final class`1102. Use `private(set) var` for state properties modified only by the ViewModel1113. Use `ViewState<T>` enum for async data — never separate boolean flags1124. Inject dependencies via constructor with protocol types1135. Use `.task { }` for initial data loading1146. Keep View bodies pure — no `Task { }` inside body, no business logic1157. Use typed `enum Route: Hashable` for navigation1168. Add `// MARK: -` sections: Properties, Init, Actions, Computed Properties1179. Import only `Foundation` (and domain modules) in ViewModels — never `SwiftUI`11810. Keep every generated file ≤ 400 lines. Extract subviews into dedicated files. Split ViewModel logic into extensions (`MyVM+Search.swift`) or child ViewModels when approaching that limit. For legacy files, log a split task in the feature's `refactoring/` plan instead of forcing it mid-refactor.11911. Before modifying a View or ViewModel, output a brief `<thought>` analyzing its current state and redraw triggers.12012. **`@Observable` instances passed to `.environment(...)` must be owned by a stable `@State`** — `.environment(AppTheme())` creates a new `AppTheme` on every parent redraw, which defeats the point of environment injection and breaks observation. Always own it: `@State private var theme = AppTheme()` then `.environment(theme)`.12113. **ViewModel signals navigation intent via closures; View owns the `@State` toggle.** Instead of `viewModel.router.push(.profile(user))`, the ViewModel exposes `var onOpenProfile: ((User) -> Void)?` and the View sets `.onOpenProfile = { user in path.append(.profile(user)) }` at wire-up time. This keeps the ViewModel ignorant of navigation mechanics (testable, reusable across hosts) while the View remains the single owner of `@State path`.12214. **`Self._printChanges()` verification steps for splitting views.** When fixing redraw storms, verification is a sequence, not a single check: (1) add `let _ = Self._printChanges()` to both the parent view body AND the split child, (2) type/tap to produce change events, (3) confirm ONLY the target child prints during the interaction — if the parent also prints, it has a hidden read of the changing value (search the body for every `store.property` access and verify equality-only reads use stored, not computed, state), (4) remove both `_printChanges` before committing.123</critical_rules>124125## Anti-Pattern Severity Reference126127When auditing a SwiftUI codebase, assign severities from this canonical table. Models systematically mis-classify these — memorize the boundaries.128129| Anti-pattern | Severity | Why this level |130|---|:---:|---|131| `import SwiftUI` in a ViewModel | 🔴 Critical | ViewModel leaks View-layer types; makes the ViewModel untestable outside a SwiftUI host |132| `UIViewController`/`UIView` reference inside a ViewModel | 🔴 Critical | Same — hard coupling to UIKit, blocks cross-platform and blocks unit testing |133| Missing `@MainActor` on a ViewModel that mutates UI-facing state | 🔴 Critical | Undefined behavior — UI reads off-main-thread state, potential crashes |134| Force-unwrap `!` on an async result in a ViewModel | 🔴 Critical | Crash vector in production |135| `viewModel` declared as plain `var` without `@State` in the owning View | 🟡 High | A new ViewModel instance is created on every parent redraw — loses all state, fires effects repeatedly |136| `.onAppear { Task { await viewModel.load() } }` | 🟡 High | Unmanaged: fires on every view appearance (back-nav, tab switch), not tied to task lifetime. Replace with `.task { }` |137| Business logic in View body (network calls, mutation) | 🟡 High | Breaks reviewability and testability, but not a crash |138| Separate `isLoading` / `error` / `data` boolean flags on a ViewModel | 🟢 Medium | Functionally works but creates impossible states (`isLoading && error != nil`) — migrate to `ViewState<T>` enum |139| Missing `// MARK: -` section comments | 🟢 Medium | Cosmetic, affects reviewability |140| `@StateObject` / `@ObservedObject` / `@EnvironmentObject` in an `@Observable` migration path | 🟢 Medium | Works with legacy `ObservableObject` but should be migrated in phased PRs |141142**Decision rules:**143- **Critical** = crash, data corruption, or blocks all testing144- **High** = not a crash, but breaks an architectural invariant that cascades (unmanaged tasks, lost state, hidden coupling)145- **Medium** = functional code with a long-tail maintenance cost or impossible states146147## Migration Mapping: `ObservableObject` → `@Observable`148149This table must appear verbatim in every "migrate from ObservableObject" response — readers copy it into code reviews.150151| Legacy (`ObservableObject`) | Modern (`@Observable`) | Note |152|---|---|---|153| `class VM: ObservableObject` | `@Observable class VM` | Add `@MainActor final` if it mutates UI state |154| `@Published var count` | `var count` | Plain `var` — the macro auto-tracks reads |155| `@StateObject var vm = VM()` in the owner | `@State private var vm = VM()` | `@State` owns the lifecycle; same semantics |156| `@ObservedObject var vm: VM` in a child that only reads | `let vm: VM` | No wrapper — plain reference, tracked automatically |157| `@ObservedObject var vm: VM` in a child that needs `$vm.property` | `@Bindable var vm: VM` | `@Bindable` enables two-way bindings without ownership |158| `@EnvironmentObject var theme: AppTheme` | `@Environment(AppTheme.self) private var theme` | Type-based lookup; env value must be registered via `.environment(themeInstance)` |159| `ObservableObject` + `@Published` tests assert on publisher changes | Reads to properties inside `withObservationTracking { }` | Modern Observation framework replaces Combine plumbing |160161**Common gotcha:** models often apply `@State` to the VM in the owning view but forget to switch `@ObservedObject` → `let` or `@Bindable` in child views — the whole chain must migrate together or child views won't observe changes.162163When generating tests, ALWAYS:1641651. Use protocol mocks with `var stubbed*` and `var *CallCount` tracking1662. Test through public interface, never test private methods1673. Mark test classes/structs `@MainActor` when testing `@MainActor` ViewModels1684. Use `await fulfillment(of:)` for async tests — NEVER `wait(for:)` (deadlocks)1695. Include memory leak detection with `addTeardownBlock { [weak sut] in XCTAssertNil(sut) }`170171## Fallback Strategies & Loop Breakers172173<fallback_strategies>174When refactoring legacy code, you may encounter stubborn Swift compiler errors. If you fail to fix the same error twice, break the loop:1751761. **@State vs @Bindable Generics:** If the compiler complains about property wrapper bindings (`$`), ensure you use `@Bindable` in subviews for `@Observable` types. Why: `@State` creates ownership (single source of truth), while `@Bindable` enables two-way bindings without ownership — the compiler enforces this distinction. If unresolved, temporarily use plain `let` and closure callbacks to unblock compilation.1772. **NavigationStack Path Issues:** If the compiler complains about `Hashable` routes or `navigationDestination` types, ensure your `enum Route` is perfectly `Hashable` and avoid passing complex models (prefer passing IDs). Why: NavigationStack serializes the path for state restoration, so every route case must be deterministically hashable.1783. **Revert and Restart:** If a View refactor spirals into 50+ compiler errors related to ambiguous type inference, stop. Propose reverting the changes and breaking the problem into two smaller phases (e.g. migrate properties first, then extract subviews). Why: SwiftUI's type inference cascades — a single change can destabilize unrelated code, and small PRs are far easier to review and debug.179</fallback_strategies>180181## Confidence Checks182183Before finalizing generated or refactored code, verify ALL:184185```186□ No duplicate functionality — searched codebase for existing implementations187□ Architecture adherence — follows patterns already established in the project188□ Naming conventions — matches existing project naming style189□ Import check — ViewModel imports only Foundation, NOT SwiftUI190□ @MainActor — present on all ViewModel class declarations191□ ViewState — used for all async data, no separate isLoading/error booleans192□ DI — dependencies injected via protocol, not accessed via singletons193□ Task management — .task modifier for lifecycle, explicit cancellation handling194□ CancellationError — handled silently, never shown to user195□ Tests — corresponding test file exists or is created alongside196□ PR scope — changes within defined scope, new findings go to `refactoring/discovered.md`197□ File size — new files ≤ 400 lines; existing oversized files have a split task logged in `refactoring/`198```199200## Companion Skills201202> **Before generating async ViewModel, Task, or actor code:** determine the project's concurrency approach. If unclear from context, ask the user.203204| Project's concurrency stack | Companion skill | Apply when |205|---|---|---|206| `async/await`, actors, Swift 6, `@MainActor` | `skills/swift-concurrency/SKILL.md` | Writing async ViewModel methods, Task creation, actor-isolated state |207| `DispatchQueue`, `OperationQueue` (legacy or hybrid) | `skills/gcd-operations/SKILL.md` | Writing queue-based networking, background work, thread-safe state |208209**If unclear, ask:** "Does this project use Swift Concurrency (async/await) or GCD for async operations?"210211## References212213| Reference | When to Read |214|-----------|-------------|215| `references/rules.md` | Do's and Don'ts quick reference: priority rules and critical anti-patterns |216| `references/mvvm-observable.md` | Creating ViewModels, @State/@Bindable ownership rules, migration mapping |217| `references/navigation.md` | Router pattern, deep linking, TabView setup, sheets |218| `references/dependency-injection.md` | @Environment, @Injected wrapper, constructor injection, testing DI |219| `references/networking.md` | ViewState enum, Repository pattern, HTTPClient, task cancellation |220| `references/anti-patterns.md` | Code review detection checklist, severity-ranked violations |221| `references/testing.md` | ViewModel unit tests, async patterns, mocks, memory leak detection |222| `references/performance.md` | Self._printChanges(), Instruments, launch time, verification evidence |223| `references/file-organization.md` | File size guidelines, extension splitting, child ViewModels, subview extraction |224| `references/refactoring-workflow.md` | `refactoring/` directory protocol, per-feature plans, PR sizing, phase ordering |