install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/pproenca/dot-skills/swift-ui-architect" ~/.claude/skills/comeonoliver-skillshub-swift-ui-architect && rm -rf "$T"
manifest:
skills/pproenca/dot-skills/swift-ui-architect/SKILL.mdsource content
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
only (neverDesignSystem
, never sibling features)Data - App target is the convergence point and owns
, concrete coordinators, and Route Shell wiringDependencyContainer
stays pure Swift and defines models plus repository,Domain
,*Coordinating
, andErrorRouting
contractsAppError
owns SwiftData/network/sync/retry/background I/O and implements Domain protocolsData- 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)
for ViewModels/coordinators,@Observable
/ObservableObject
never@Published- No dedicated use-case/interactor layer: ViewModels call Domain repository protocols directly
- Coordinator protocols live in Domain; concrete coordinators own
in App targetNavigationPath - Route shells live in App target and own
mapping.navigationDestination
+AppError
drive presentation policy; ViewModels do not hardcode global error UIErrorRouting- 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 | | 6 |
| 2 | State Architecture | CRITICAL | | 7 |
| 3 | View Composition | HIGH | | 6 |
| 4 | Navigation & Coordination | HIGH | | 5 |
| 5 | Layer Architecture | HIGH | | 6 |
| 6 | Dependency Injection | MEDIUM-HIGH | | 4 |
| 7 | List & Collection Performance | MEDIUM | | 4 |
| 8 | Async & Data Flow | MEDIUM | | 5 |
Quick Reference
1. View Identity & Diffing (CRITICAL)
- Apply @Equatable macro to every SwiftUI viewdiff-equatable-views
- Use @SkipEquatable for closure/handler propertiesdiff-closure-skip
- Never store reference types without Equatable conformancediff-reference-types
- Use stable O(1) identifiers in ForEachdiff-identity-stability
- Never use AnyView — use @ViewBuilder or genericsdiff-avoid-anyview
- Use _printChanges() to diagnose unnecessary re-rendersdiff-printchanges-debug
2. State Architecture (CRITICAL)
- Use @Observable classes for all ViewModelsstate-observable-class
- @State for owned data, plain property for injected datastate-ownership
- One source of truth per piece of statestate-single-source
- Leverage @Observable property-level trackingstate-scoped-observation
- Pass @Binding only for two-way data flowstate-binding-minimal
- Use @Environment for app-wide shared dependenciesstate-environment-global
- Never use @Published or ObservableObjectstate-no-published
3. View Composition (HIGH)
- Maximum 10 nodes in view bodyview-body-complexity
- Extract computed properties/helpers into separate View structsview-extract-subviews
- Zero business logic in bodyview-no-logic-in-body
- Pass only needed properties, not entire modelsview-minimal-dependencies
- Use @ViewBuilder for conditional compositionview-viewbuilder-composition
- Never perform work in View initview-no-init-sideeffects
4. Navigation & Coordination (HIGH)
- Every feature has a coordinator owning NavigationStacknav-coordinator-pattern
- Define all routes as a Hashable enumnav-routes-enum
- Coordinators must support URL-based deep linkingnav-deeplink-support
- Present modals via coordinator, not inlinenav-modal-sheets
- Never use NavigationLink(destination:) — use navigationDestination(for:)nav-no-navigationlink
5. Layer Architecture (HIGH)
- Domain layer has zero framework importslayer-dependency-rule
- Do not add a use-case layer; keep orchestration in ViewModel + repository protocolslayer-usecase-protocol
- Repository protocols in Domain, implementations in Datalayer-repository-protocol
- Domain models are structs, never classeslayer-model-value-types
- Views never access repositories directly; ViewModel calls repository protocolslayer-no-view-repository
- ViewModels expose display-ready state onlylayer-viewmodel-boundary
6. Dependency Injection (MEDIUM-HIGH)
- Inject container-managed protocol dependencies via @Environmentdi-environment-injection
- All injected dependencies are protocol typesdi-protocol-abstraction
- Composedi-container-composition
in App target and expose VM factoriesDependencyContainer
- Every protocol dependency has a mock for testingdi-mock-testing
7. List & Collection Performance (MEDIUM)
- ForEach must produce constant view count per elementlist-constant-viewcount
- Filter/sort in ViewModel, never inside ForEachlist-filter-in-model
- Use LazyVStack/LazyHStack for unbounded contentlist-lazy-stacks
- Provide explicit id keyPath — never rely on implicit identitylist-id-keypath
8. Async & Data Flow (MEDIUM)
- Usedata-task-modifier
as the primary feature data-loading trigger.task(id:)
- Never perform async work in initdata-async-init
- Model loading states as enum, not booleansdata-error-loadable
- Prefer async/await over Combine for new codedata-combine-avoid
- Use .task automatic cancellation — never manage Tasks manuallydata-cancellation
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 |