SwiftUI Modular MVVM-C Architecture
Opinionated architecture enforcement for SwiftUI clinic-style apps. This skill aligns to the iOS 26 / Swift 6.2 clinic architecture: modular MVVM-C in local SPM packages, concrete coordinators and route shells in the App target, pure Domain protocols, and Data as the only I/O layer.
Mandated Architecture Stack
┌───────────────────────────────────────────────────────────────┐
│ App target: DependencyContainer, Coordinators, Route Shells │
├───────────────┬───────────────┬───────────────┬──────────────┤
│ Feature* SPM │ Feature* SPM │ Feature* SPM │ Feature* SPM │
│ View + VM │ View + VM │ View + VM │ View + VM │
├───────────────────────────────────────────────────────────────┤
│ Data SPM: repository impls, remote/local, retry, sync queue │
├───────────────────────────────────────────────────────────────┤
│ Domain SPM: models, repository protocols, coordinator protocols│
│ and ErrorRouting/AppError │
├───────────────────────────────────────────────────────────────┤
│ Shared SPMs: DesignSystem, SharedKit │
└───────────────────────────────────────────────────────────────┘
Dependency Rule: Feature modules import Domain + DesignSystem only. Features never import Data or other features. App target is the only convergence point.
Clinic Architecture Contract (iOS 26 / Swift 6.2)
All guidance in this skill assumes the clinic modular MVVM-C architecture:
- Feature modules import
Domain + DesignSystem only (never Data, never sibling features)
- App target is the convergence point and owns
DependencyContainer, concrete coordinators, and Route Shell wiring
Domain stays pure Swift and defines models plus repository, *Coordinating, ErrorRouting, and AppError contracts
Data owns SwiftData/network/sync/retry/background I/O and implements Domain protocols
- Read/write flow defaults to stale-while-revalidate reads and optimistic queued writes
- ViewModels call repository protocols directly (no default use-case/interactor layer)
When to Apply
Reference these guidelines when:
- Building or refactoring feature modules under local SPM packages
- Wiring coordinators, route shells, and dependency container factories
- Defining Domain protocols for repositories, coordinators, and error routing
- Enforcing Data-only ownership of networking, persistence, and sync
- Reviewing stale-while-revalidate reads and optimistic queued writes
Non-Negotiable Constraints (iOS 26 / Swift 6.2)
@Observable for ViewModels/coordinators, ObservableObject / @Published never
- No dedicated use-case/interactor layer: ViewModels call Domain repository protocols directly
- Coordinator protocols live in Domain; concrete coordinators own
NavigationPath in App target
- Route shells live in App target and own
.navigationDestination mapping
AppError + ErrorRouting drive presentation policy; ViewModels do not hardcode global error UI
- SwiftData / URLSession / retry / sync queue logic stays in Data package only
Rule Categories by Priority
| Priority |
Category |
Impact |
Prefix |
Rules |
| 1 |
View Identity & Diffing |
CRITICAL |
diff- |
6 |
| 2 |
State Architecture |
CRITICAL |
state- |
7 |
| 3 |
View Composition |
HIGH |
view- |
6 |
| 4 |
Navigation & Coordination |
HIGH |
nav- |
5 |
| 5 |
Layer Architecture |
HIGH |
layer- |
6 |
| 6 |
Dependency Injection |
MEDIUM-HIGH |
di- |
4 |
| 7 |
List & Collection Performance |
MEDIUM |
list- |
4 |
| 8 |
Async & Data Flow |
MEDIUM |
data- |
5 |
Quick Reference
1. View Identity & Diffing (CRITICAL)
diff-equatable-views - Apply @Equatable macro to every SwiftUI view
diff-closure-skip - Use @SkipEquatable for closure/handler properties
diff-reference-types - Never store reference types without Equatable conformance
diff-identity-stability - Use stable O(1) identifiers in ForEach
diff-avoid-anyview - Never use AnyView — use @ViewBuilder or generics
diff-printchanges-debug - Use _printChanges() to diagnose unnecessary re-renders
2. State Architecture (CRITICAL)
state-observable-class - Use @Observable classes for all ViewModels
state-ownership - @State for owned data, plain property for injected data
state-single-source - One source of truth per piece of state
state-scoped-observation - Leverage @Observable property-level tracking
state-binding-minimal - Pass @Binding only for two-way data flow
state-environment-global - Use @Environment for app-wide shared dependencies
state-no-published - Never use @Published or ObservableObject
3. View Composition (HIGH)
view-body-complexity - Maximum 10 nodes in view body
view-extract-subviews - Extract computed properties/helpers into separate View structs
view-no-logic-in-body - Zero business logic in body
view-minimal-dependencies - Pass only needed properties, not entire models
view-viewbuilder-composition - Use @ViewBuilder for conditional composition
view-no-init-sideeffects - Never perform work in View init
4. Navigation & Coordination (HIGH)
nav-coordinator-pattern - Every feature has a coordinator owning NavigationStack
nav-routes-enum - Define all routes as a Hashable enum
nav-deeplink-support - Coordinators must support URL-based deep linking
nav-modal-sheets - Present modals via coordinator, not inline
nav-no-navigationlink - Never use NavigationLink(destination:) — use navigationDestination(for:)
5. Layer Architecture (HIGH)
layer-dependency-rule - Domain layer has zero framework imports
layer-usecase-protocol - Do not add a use-case layer; keep orchestration in ViewModel + repository protocols
layer-repository-protocol - Repository protocols in Domain, implementations in Data
layer-model-value-types - Domain models are structs, never classes
layer-no-view-repository - Views never access repositories directly; ViewModel calls repository protocols
layer-viewmodel-boundary - ViewModels expose display-ready state only
6. Dependency Injection (MEDIUM-HIGH)
di-environment-injection - Inject container-managed protocol dependencies via @Environment
di-protocol-abstraction - All injected dependencies are protocol types
di-container-composition - Compose DependencyContainer in App target and expose VM factories
di-mock-testing - Every protocol dependency has a mock for testing
7. List & Collection Performance (MEDIUM)
list-constant-viewcount - ForEach must produce constant view count per element
list-filter-in-model - Filter/sort in ViewModel, never inside ForEach
list-lazy-stacks - Use LazyVStack/LazyHStack for unbounded content
list-id-keypath - Provide explicit id keyPath — never rely on implicit identity
8. Async & Data Flow (MEDIUM)
data-task-modifier - Use .task(id:) as the primary feature data-loading trigger
data-async-init - Never perform async work in init
data-error-loadable - Model loading states as enum, not booleans
data-combine-avoid - Prefer async/await over Combine for new code
data-cancellation - Use .task automatic cancellation — never manage Tasks manually
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File |
Description |
| references/_sections.md |
Category definitions and ordering |
| assets/templates/_template.md |
Template for new rules |
| metadata.json |
Version and reference information |
1---2name: swift-ui-architect3description: Opinionated SwiftUI architecture enforcement for iOS 26 / Swift 6.2 clinic modular MVVM-C apps using local SPM package boundaries. Enforces App-target `DependencyContainer` + route shells, @Observable ViewModels/coordinators, Domain repository/coordinator/error-routing protocols, Data-owned I/O, stale-while-revalidate reads, and optimistic queued sync. Use when writing, reviewing, or refactoring SwiftUI architecture, navigation, dependency wiring, or repository boundaries.4---5
6# SwiftUI Modular MVVM-C Architecture
7
8Opinionated architecture enforcement for SwiftUI clinic-style apps. This skill aligns to the iOS 26 / Swift 6.2 clinic architecture: modular MVVM-C in local SPM packages, concrete coordinators and route shells in the App target, pure Domain protocols, and Data as the only I/O layer.
9
10## Mandated Architecture Stack
11
12```
13┌───────────────────────────────────────────────────────────────┐
14│ App target: DependencyContainer, Coordinators, Route Shells │
15├───────────────┬───────────────┬───────────────┬──────────────┤
16│ Feature* SPM │ Feature* SPM │ Feature* SPM │ Feature* SPM │
17│ View + VM │ View + VM │ View + VM │ View + VM │
18├───────────────────────────────────────────────────────────────┤
19│ Data SPM: repository impls, remote/local, retry, sync queue │
20├───────────────────────────────────────────────────────────────┤
21│ Domain SPM: models, repository protocols, coordinator protocols│
22│ and ErrorRouting/AppError │
23├───────────────────────────────────────────────────────────────┤
24│ Shared SPMs: DesignSystem, SharedKit │
25└───────────────────────────────────────────────────────────────┘
26```
27
28**Dependency Rule**: Feature modules import `Domain` + `DesignSystem` only. Features never import `Data` or other features. App target is the only convergence point.
29
30
31## Clinic Architecture Contract (iOS 26 / Swift 6.2)
32
33All guidance in this skill assumes the clinic modular MVVM-C architecture:
34
35- Feature modules import `Domain` + `DesignSystem` only (never `Data`, never sibling features)
36- App target is the convergence point and owns `DependencyContainer`, concrete coordinators, and Route Shell wiring
37- `Domain` stays pure Swift and defines models plus repository, `*Coordinating`, `ErrorRouting`, and `AppError` contracts
38- `Data` owns SwiftData/network/sync/retry/background I/O and implements Domain protocols
39- Read/write flow defaults to stale-while-revalidate reads and optimistic queued writes
40- ViewModels call repository protocols directly (no default use-case/interactor layer)
41
42## When to Apply
43
44Reference these guidelines when:
45- Building or refactoring feature modules under local SPM packages
46- Wiring coordinators, route shells, and dependency container factories
47- Defining Domain protocols for repositories, coordinators, and error routing
48- Enforcing Data-only ownership of networking, persistence, and sync
49- Reviewing stale-while-revalidate reads and optimistic queued writes
50
51## Non-Negotiable Constraints (iOS 26 / Swift 6.2)
52
53- `@Observable` for ViewModels/coordinators, `ObservableObject` / `@Published` never
54- No dedicated use-case/interactor layer: ViewModels call Domain repository protocols directly
55- Coordinator protocols live in Domain; concrete coordinators own `NavigationPath` in App target
56- Route shells live in App target and own `.navigationDestination` mapping
57- `AppError` + `ErrorRouting` drive presentation policy; ViewModels do not hardcode global error UI
58- SwiftData / URLSession / retry / sync queue logic stays in Data package only
59
60## Rule Categories by Priority
61
62| Priority | Category | Impact | Prefix | Rules |
63|----------|----------|--------|--------|-------|
64| 1 | View Identity & Diffing | CRITICAL | `diff-` | 6 |
65| 2 | State Architecture | CRITICAL | `state-` | 7 |
66| 3 | View Composition | HIGH | `view-` | 6 |
67| 4 | Navigation & Coordination | HIGH | `nav-` | 5 |
68| 5 | Layer Architecture | HIGH | `layer-` | 6 |
69| 6 | Dependency Injection | MEDIUM-HIGH | `di-` | 4 |
70| 7 | List & Collection Performance | MEDIUM | `list-` | 4 |
71| 8 | Async & Data Flow | MEDIUM | `data-` | 5 |
72
73## Quick Reference
74
75### 1. View Identity & Diffing (CRITICAL)
76
77- [`diff-equatable-views`](references/diff-equatable-views.md) - Apply @Equatable macro to every SwiftUI view
78- [`diff-closure-skip`](references/diff-closure-skip.md) - Use @SkipEquatable for closure/handler properties
79- [`diff-reference-types`](references/diff-reference-types.md) - Never store reference types without Equatable conformance
80- [`diff-identity-stability`](references/diff-identity-stability.md) - Use stable O(1) identifiers in ForEach
81- [`diff-avoid-anyview`](references/diff-avoid-anyview.md) - Never use AnyView — use @ViewBuilder or generics
82- [`diff-printchanges-debug`](references/diff-printchanges-debug.md) - Use _printChanges() to diagnose unnecessary re-renders
83
84### 2. State Architecture (CRITICAL)
85
86- [`state-observable-class`](references/state-observable-class.md) - Use @Observable classes for all ViewModels
87- [`state-ownership`](references/state-ownership.md) - @State for owned data, plain property for injected data
88- [`state-single-source`](references/state-single-source.md) - One source of truth per piece of state
89- [`state-scoped-observation`](references/state-scoped-observation.md) - Leverage @Observable property-level tracking
90- [`state-binding-minimal`](references/state-binding-minimal.md) - Pass @Binding only for two-way data flow
91- [`state-environment-global`](references/state-environment-global.md) - Use @Environment for app-wide shared dependencies
92- [`state-no-published`](references/state-no-published.md) - Never use @Published or ObservableObject
93
94### 3. View Composition (HIGH)
95
96- [`view-body-complexity`](references/view-body-complexity.md) - Maximum 10 nodes in view body
97- [`view-extract-subviews`](references/view-extract-subviews.md) - Extract computed properties/helpers into separate View structs
98- [`view-no-logic-in-body`](references/view-no-logic-in-body.md) - Zero business logic in body
99- [`view-minimal-dependencies`](references/view-minimal-dependencies.md) - Pass only needed properties, not entire models
100- [`view-viewbuilder-composition`](references/view-viewbuilder-composition.md) - Use @ViewBuilder for conditional composition
101- [`view-no-init-sideeffects`](references/view-no-init-sideeffects.md) - Never perform work in View init
102
103### 4. Navigation & Coordination (HIGH)
104
105- [`nav-coordinator-pattern`](references/nav-coordinator-pattern.md) - Every feature has a coordinator owning NavigationStack
106- [`nav-routes-enum`](references/nav-routes-enum.md) - Define all routes as a Hashable enum
107- [`nav-deeplink-support`](references/nav-deeplink-support.md) - Coordinators must support URL-based deep linking
108- [`nav-modal-sheets`](references/nav-modal-sheets.md) - Present modals via coordinator, not inline
109- [`nav-no-navigationlink`](references/nav-no-navigationlink.md) - Never use NavigationLink(destination:) — use navigationDestination(for:)
110
111### 5. Layer Architecture (HIGH)
112
113- [`layer-dependency-rule`](references/layer-dependency-rule.md) - Domain layer has zero framework imports
114- [`layer-usecase-protocol`](references/layer-usecase-protocol.md) - Do not add a use-case layer; keep orchestration in ViewModel + repository protocols
115- [`layer-repository-protocol`](references/layer-repository-protocol.md) - Repository protocols in Domain, implementations in Data
116- [`layer-model-value-types`](references/layer-model-value-types.md) - Domain models are structs, never classes
117- [`layer-no-view-repository`](references/layer-no-view-repository.md) - Views never access repositories directly; ViewModel calls repository protocols
118- [`layer-viewmodel-boundary`](references/layer-viewmodel-boundary.md) - ViewModels expose display-ready state only
119
120### 6. Dependency Injection (MEDIUM-HIGH)
121
122- [`di-environment-injection`](references/di-environment-injection.md) - Inject container-managed protocol dependencies via @Environment
123- [`di-protocol-abstraction`](references/di-protocol-abstraction.md) - All injected dependencies are protocol types
124- [`di-container-composition`](references/di-container-composition.md) - Compose `DependencyContainer` in App target and expose VM factories
125- [`di-mock-testing`](references/di-mock-testing.md) - Every protocol dependency has a mock for testing
126
127### 7. List & Collection Performance (MEDIUM)
128
129- [`list-constant-viewcount`](references/list-constant-viewcount.md) - ForEach must produce constant view count per element
130- [`list-filter-in-model`](references/list-filter-in-model.md) - Filter/sort in ViewModel, never inside ForEach
131- [`list-lazy-stacks`](references/list-lazy-stacks.md) - Use LazyVStack/LazyHStack for unbounded content
132- [`list-id-keypath`](references/list-id-keypath.md) - Provide explicit id keyPath — never rely on implicit identity
133
134### 8. Async & Data Flow (MEDIUM)
135
136- [`data-task-modifier`](references/data-task-modifier.md) - Use `.task(id:)` as the primary feature data-loading trigger
137- [`data-async-init`](references/data-async-init.md) - Never perform async work in init
138- [`data-error-loadable`](references/data-error-loadable.md) - Model loading states as enum, not booleans
139- [`data-combine-avoid`](references/data-combine-avoid.md) - Prefer async/await over Combine for new code
140- [`data-cancellation`](references/data-cancellation.md) - Use .task automatic cancellation — never manage Tasks manually
141
142## How to Use
143
144Read individual reference files for detailed explanations and code examples:
145
146- [Section definitions](references/_sections.md) - Category structure and impact levels
147- [Rule template](assets/templates/_template.md) - Template for adding new rules
148
149## Reference Files
150
151| File | Description |
152|------|-------------|
153| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
154| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
155| [metadata.json](metadata.json) | Version and reference information |