swift-ui-architect
Original:🇺🇸 English
Translated
Opinionated SwiftUI architecture enforcement for iOS 17+ apps using Clean MVVM + Coordinator pattern. Enforces Airbnb's @Equatable diffing, @Observable state, NavigationStack coordinators, and Clean Architecture layer boundaries. This skill should be used when writing, reviewing, or refactoring SwiftUI code. Triggers on tasks involving SwiftUI views, ViewModels, navigation, state management, dependency injection, or iOS app architecture.
10installs
Sourcepproenca/dot-skills
Added on
NPX Install
npx skill4agent add pproenca/dot-skills swift-ui-architectTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Airbnb SwiftUI Best Practices
Opinionated, strict architectural enforcement for SwiftUI iOS apps. Contains 43 rules across 8 categories, derived from Airbnb Engineering, Apple WWDC sessions, Clean Architecture patterns, and Advanced iOS App Architecture (raywenderlich). This skill mandates a single, non-negotiable architecture stack.
Mandated Architecture Stack
┌─────────────────────────────────────────────────────┐
│ Presentation Layer (SwiftUI Views) │
│ ├── Views: @Equatable, decomposed, minimal bodies │
│ ├── ViewModels: @Observable classes via @State │
│ └── Coordinators: NavigationStack + enum routes │
├─────────────────────────────────────────────────────┤
│ Domain Layer (Pure Swift) │
│ ├── Use Cases / Interactors (stateless, protocol) │
│ ├── Domain Models (value types, Equatable) │
│ └── Repository Protocols (abstractions only) │
├─────────────────────────────────────────────────────┤
│ Data Layer (Implementation) │
│ ├── Repository Implementations │
│ ├── Network Services │
│ └── Persistence (SwiftData / CoreData) │
└─────────────────────────────────────────────────────┘Dependency Rule: Arrows point inward only. Views -> Domain <- Data. Domain has zero framework imports.
When to Apply
Reference these guidelines when:
- Writing any new SwiftUI view, ViewModel, or coordinator
- Creating or modifying navigation flows
- Adding data fetching, state management, or dependency injection
- Reviewing code for architecture compliance or performance
- Refactoring existing SwiftUI code toward this architecture
Non-Negotiable Constraints (iOS 17+)
- everywhere,
@Observable/ObservableObjectnever@Published - + coordinator pattern,
NavigationStackneverNavigationLink(destination:) - macro on every view,
@EquatableneverAnyView - Domain layer has zero SwiftUI/UIKit imports
- Views never access repositories directly
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 view
diff-equatable-views - - Use @SkipEquatable for closure/handler properties
diff-closure-skip - - Never store reference types without Equatable conformance
diff-reference-types - - Use stable O(1) identifiers in ForEach
diff-identity-stability - - Never use AnyView — use @ViewBuilder or generics
diff-avoid-anyview - - Use _printChanges() to diagnose unnecessary re-renders
diff-printchanges-debug
2. State Architecture (CRITICAL)
- - Use @Observable classes for all ViewModels
state-observable-class - - @State for owned data, plain property for injected data
state-ownership - - One source of truth per piece of state
state-single-source - - Leverage @Observable property-level tracking
state-scoped-observation - - Pass @Binding only for two-way data flow
state-binding-minimal - - Use @Environment for app-wide shared dependencies
state-environment-global - - Never use @Published or ObservableObject
state-no-published
3. View Composition (HIGH)
- - Maximum 10 nodes in view body
view-body-complexity - - Extract computed properties/helpers into separate View structs
view-extract-subviews - - Zero business logic in body
view-no-logic-in-body - - Pass only needed properties, not entire models
view-minimal-dependencies - - Use @ViewBuilder for conditional composition
view-viewbuilder-composition - - Never perform work in View init
view-no-init-sideeffects
4. Navigation & Coordination (HIGH)
- - Every feature has a coordinator owning NavigationStack
nav-coordinator-pattern - - Define all routes as a Hashable enum
nav-routes-enum - - Coordinators must support URL-based deep linking
nav-deeplink-support - - Present modals via coordinator, not inline
nav-modal-sheets - - Never use NavigationLink(destination:) — use navigationDestination(for:)
nav-no-navigationlink
5. Layer Architecture (HIGH)
- - Domain layer has zero framework imports
layer-dependency-rule - - Every use case is a protocol with a single execute method
layer-usecase-protocol - - Repository protocols in Domain, implementations in Data
layer-repository-protocol - - Domain models are structs, never classes
layer-model-value-types - - Views never access repositories directly
layer-no-view-repository - - ViewModels expose display-ready state only
layer-viewmodel-boundary
6. Dependency Injection (MEDIUM-HIGH)
- - Inject via @Environment with custom EnvironmentKey
di-environment-injection - - All injected dependencies are protocol types
di-protocol-abstraction - - Compose dependency container at app root
di-container-composition - - Every protocol dependency has a mock for testing
di-mock-testing
7. List & Collection Performance (MEDIUM)
- - ForEach must produce constant view count per element
list-constant-viewcount - - Filter/sort in ViewModel, never inside ForEach
list-filter-in-model - - Use LazyVStack/LazyHStack for unbounded content
list-lazy-stacks - - Provide explicit id keyPath — never rely on implicit identity
list-id-keypath
8. Async & Data Flow (MEDIUM)
- - Use .task {} for async data loading
data-task-modifier - - Never perform async work in init
data-async-init - - Model loading states as enum, not booleans
data-error-loadable - - Prefer async/await over Combine for new code
data-combine-avoid - - Use .task automatic cancellation — never manage Tasks manually
data-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 |