Swift MVVM Skill
Help the agent produce better MVVM code for Swift projects.
Core stance
- Keep the ViewModel UI-framework agnostic. ViewModels should not import
SwiftUI, UIKit, or AppKit.
- The ViewModel should be mostly (1) state, (2) intent methods, and (3) dependency coordination.
- Push work into smaller, testable units (pure structs/functions, use cases, controllers, repositories, mappers, formatters).
- Use dependency injection with protocols so everything is mockable.
- Extensions are encouraged for organization (especially protocol conformances).
When to use
Use this skill when the user asks to:
- Add or refactor features in SwiftUI/UIKit/AppKit and keep architecture clean.
- Create or improve a ViewModel (state, intents, effects) and move logic out of Views/ViewControllers.
- Fix state-management issues (wrong wrappers, threading warnings, unstable bindings).
- Improve testability (protocol-based dependencies, mocks/fakes, deterministic state updates).
- Reduce a “massive ViewModel” by splitting concerns.
First check (before writing code)
- Identify UI tech: SwiftUI, UIKit, or AppKit.
- Identify deployment target:
- If iOS 17+/macOS 14+ is available, prefer Observation (
@Observable).
- Otherwise prefer Combine-based
ObservableObject + @Published.
- Match existing project style (naming, folders, DI approach, networking layer).
MVVM responsibilities
View layer
SwiftUI View / UIKit UIViewController / AppKit NSViewController:
- Declares layout and binds to state.
- Sends user intents (tap, selection, text changes) to the ViewModel.
- Owns UI-only concerns (navigation, presenting alerts, AppKit panels, first responder, etc.).
ViewModel
- Owns screen state (loading/data/error, derived UI values).
- Coordinates effects through injected dependencies.
- Exposes intent methods (
onAppear(), refresh(), didTap…) rather than views calling random internals.
Domain / Services
- Encapsulate fetching, caching, persistence, decoding, validation.
- Must be independent of UI frameworks.
Hard rule: No UI framework imports in ViewModels
ViewModels should not import:
If a ViewModel needs a platform behavior, define a tiny protocol in a non-UI module (Foundation-only), and provide a platform implementation in the View layer (or platform adapter module).
Example:
// In a Foundation-only target / file.
protocol FileRevealing {
func reveal(_ url: URL)
}
// In AppKit layer.
import AppKit
struct WorkspaceFileRevealer: FileRevealing {
func reveal(_ url: URL) {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
Any “AppKit-y” or “UIKit-y” behavior inside the ViewModel is a **smell**.
## Preferred ViewModel shapes
Pick one based on scope.
### Pattern A: Simple screen
- `State` struct (nested)
- intent methods
- async `load()` with cancellation
### Pattern B: Complex screen
- `State` struct (nested)
- `Action` enum + `send(_:)`
- reducer-like switch for state transitions
- side effects delegated to injected units
## Keep state structured (avoid a ViewModel with 30 vars)
Prefer:
```swift
struct State: Equatable {
var view = ViewState()
var content = ContentState()
var alerts = AlertsState()
struct ViewState: Equatable {
var isLoading = false
var title = ""
}
struct ContentState: Equatable {
var rows: [Row] = []
var emptyMessage: String? = nil
}
struct AlertsState: Equatable {
var error: ErrorState? = nil
}
}
Refactoring a massive ViewModel (priority order)
When reducing a large VM, refactor in this order:
- Extract pure logic first
- Move non-IO computations into pure structs or pure functions.
- Examples: filtering, sorting, mapping domain models to row models, formatting, validation, state reducers.
- Extract side effects into controllers (still testable)
- Create small “effect” units that do IO and orchestration.
- Keep them behind protocols and inject into the VM.
- Examples:
LoadExamplesUseCase, ExamplesController, AnalyticsTracking, FileRevealing.
- Leave the VM as state + intents
- VMs forward intents to pure logic/effect units and assign state.
Concurrency & cancellation rules
- UI state changes should happen on the main actor.
- Store ongoing tasks and cancel them when a new request starts or when the view disappears.
SwiftUI integration rules
Observation (@Observable)
- Hold a ViewModel instance using
@State (owner) and pass it down.
Combine (ObservableObject)
- Use
@StateObject when the view creates/owns the ViewModel.
- Use
@ObservedObject when the view is given the ViewModel.
Dependency injection rules
- ViewModel takes dependencies in its initializer.
- Dependencies are protocols; provide a production implementation and a mock/fake for tests.
- Prefer injecting small units:
- Pure logic:
RowBuilder, Validator, Reducer
- Effects:
UseCase, Controller, Repository
- Platform adapters:
FileRevealing, URLOpening, etc.
Avoid massive ViewModels
Smells:
- Imports UI frameworks.
- Does URLSession/JSON decoding directly.
- Builds NSAlert/UIAlertController.
- Knows about NSWorkspace/UIApplication.
- Formats everything inline with complex logic.
- Handles navigation, analytics, networking, caching all together.
Refactor moves:
- Extract a pure
RowBuilder or Reducer.
- Extract a
UseCase for business rules.
- Extract a
Repository for IO.
- Extract a side-effects
Controller to orchestrate multiple services.
- Extract platform adapters (tiny protocols) for UI actions.
Output expectations
When writing/refactoring:
- Provide code in diff-friendly chunks, grouped by file.
- Prefer small, composable functions.
- Use extensions for organization (e.g., protocol conformances, grouping helpers).
- Name clearly:
FooViewModel, FooState, FooUseCase, FooRepository, FooController, FooRowBuilder.
- Add tests for ViewModel state transitions and extracted pure logic.
Testing guidance
Prefer Swift Testing (import Testing, @Test, #expect, #require) for new tests.
- Test pure logic units directly (fast, deterministic).
- Test ViewModel state transitions (success/failure/cancellation) by injecting mocks.
Templates
Copy/paste from:
templates/ObservationViewModel.swift
templates/CombineViewModel.swift
templates/AppKitViewController.swift
templates/ProtocolAdapters.swift
templates/ServiceUseCaseControllerAndPureLogic.swift
templates/ViewModelTests.swift (Swift Testing)
Additional resources (load only if needed)
- MVVM Overview: references/mvvm-overview.md
- Module Structure: references/module-structure.md
- File Naming Conventions: references/file-naming-conventions.md
- Where Things Go: references/where-things-go.md
- Common Patterns: references/common-patterns.md
- Adding New Features: references/adding-new-features.md
- Integration Patterns: references/integration-patterns.md
- Anti-Patterns: references/anti-patterns.md
- Testing Considerations: references/testing-considerations.md
- Services vs Feature Services: references/services-vs-feature-services.md
- Controller vs Coordinator: references/controller-vs-coordinator.md
- State Management: references/state-management.md
1---2name: swift-mvvm3description: Use when writing or refactoring Swift (SwiftUI/UIKit/AppKit) code to follow MVVM with a small, testable ViewModel. Triggers on: MVVM, ViewModel, ObservableObject, @Observable, Observation, AppKit, NSViewController, UIKit, state management, dependency injection, protocol adapters, refactor view logic, massive view model, testable, async/await, Combine, Swift Testing, #expect.4---56# Swift MVVM Skill78Help the agent produce **better MVVM code** for Swift projects.910## Core stance11- **Keep the ViewModel UI-framework agnostic.** ViewModels should **not** import `SwiftUI`, `UIKit`, or `AppKit`.12- The ViewModel should be mostly **(1) state**, **(2) intent methods**, and **(3) dependency coordination**.13- Push work into **smaller, testable units** (pure structs/functions, use cases, controllers, repositories, mappers, formatters).14- Use **dependency injection** with protocols so everything is mockable.15- **Extensions are encouraged** for organization (especially protocol conformances).1617## When to use18Use this skill when the user asks to:19- Add or refactor features in SwiftUI/UIKit/AppKit and keep architecture clean.20- Create or improve a ViewModel (state, intents, effects) and move logic out of Views/ViewControllers.21- Fix state-management issues (wrong wrappers, threading warnings, unstable bindings).22- Improve testability (protocol-based dependencies, mocks/fakes, deterministic state updates).23- Reduce a “massive ViewModel” by splitting concerns.2425## First check (before writing code)261. Identify UI tech: **SwiftUI**, **UIKit**, or **AppKit**.272. Identify deployment target:28 - If **iOS 17+/macOS 14+** is available, prefer **Observation** (`@Observable`).29 - Otherwise prefer **Combine-based** `ObservableObject` + `@Published`.303. Match existing project style (naming, folders, DI approach, networking layer).3132## MVVM responsibilities33### View layer34SwiftUI `View` / UIKit `UIViewController` / AppKit `NSViewController`:35- Declares layout and binds to **state**.36- Sends **user intents** (tap, selection, text changes) to the ViewModel.37- Owns UI-only concerns (navigation, presenting alerts, AppKit panels, first responder, etc.).3839### ViewModel40- Owns **screen state** (loading/data/error, derived UI values).41- Coordinates effects through injected dependencies.42- Exposes **intent methods** (`onAppear()`, `refresh()`, `didTap…`) rather than views calling random internals.4344### Domain / Services45- Encapsulate fetching, caching, persistence, decoding, validation.46- Must be independent of UI frameworks.4748## Hard rule: No UI framework imports in ViewModels49**ViewModels should not import:**50- `SwiftUI`51- `UIKit`52- `AppKit`5354If a ViewModel needs a platform behavior, define a tiny protocol in a non-UI module (Foundation-only), and provide a platform implementation in the View layer (or platform adapter module).5556Example:5758```swift59// In a Foundation-only target / file.60protocol FileRevealing {61 func reveal(_ url: URL)62}6364// In AppKit layer.65import AppKit6667struct WorkspaceFileRevealer: FileRevealing {68 func reveal(_ url: URL) {69 NSWorkspace.shared.activateFileViewerSelecting([url])70 }71}7273Any “AppKit-y” or “UIKit-y” behavior inside the ViewModel is a **smell**.7475## Preferred ViewModel shapes7677Pick one based on scope.7879### Pattern A: Simple screen8081- `State` struct (nested)82- intent methods83- async `load()` with cancellation8485### Pattern B: Complex screen8687- `State` struct (nested)88- `Action` enum + `send(_:)`89- reducer-like switch for state transitions90- side effects delegated to injected units9192## Keep state structured (avoid a ViewModel with 30 vars)9394Prefer:9596```swift97struct State: Equatable {98 var view = ViewState()99 var content = ContentState()100 var alerts = AlertsState()101102 struct ViewState: Equatable {103 var isLoading = false104 var title = ""105 }106107 struct ContentState: Equatable {108 var rows: [Row] = []109 var emptyMessage: String? = nil110 }111112 struct AlertsState: Equatable {113 var error: ErrorState? = nil114 }115}116```117118## Refactoring a massive ViewModel (priority order)119120When reducing a large VM, refactor in this order:1211221. **Extract pure logic first**123124- Move non-IO computations into **pure structs** or **pure functions**.125- Examples: filtering, sorting, mapping domain models to row models, formatting, validation, state reducers.1261272. **Extract side effects into controllers (still testable)**128129- Create small “effect” units that do IO and orchestration.130- Keep them behind protocols and inject into the VM.131- Examples: `LoadExamplesUseCase`, `ExamplesController`, `AnalyticsTracking`, `FileRevealing`.1321333. **Leave the VM as state + intents**134135- VMs forward intents to pure logic/effect units and assign state.136137## Concurrency & cancellation rules138139- UI state changes should happen on the **main actor**.140- Store ongoing tasks and cancel them when a new request starts or when the view disappears.141142## SwiftUI integration rules143144### Observation (`@Observable`)145146- Hold a ViewModel instance using `@State` (owner) and pass it down.147148### Combine (`ObservableObject`)149150- Use `@StateObject` when the view **creates/owns** the ViewModel.151- Use `@ObservedObject` when the view is **given** the ViewModel.152153## Dependency injection rules154155- ViewModel takes dependencies in its initializer.156- Dependencies are protocols; provide a production implementation and a mock/fake for tests.157- Prefer injecting **small units**:158 - Pure logic: `RowBuilder`, `Validator`, `Reducer`159 - Effects: `UseCase`, `Controller`, `Repository`160 - Platform adapters: `FileRevealing`, `URLOpening`, etc.161162## Avoid massive ViewModels163164Smells:165166- Imports UI frameworks.167- Does URLSession/JSON decoding directly.168- Builds NSAlert/UIAlertController.169- Knows about NSWorkspace/UIApplication.170- Formats everything inline with complex logic.171- Handles navigation, analytics, networking, caching all together.172173Refactor moves:174175- Extract a pure `RowBuilder` or `Reducer`.176- Extract a `UseCase` for business rules.177- Extract a `Repository` for IO.178- Extract a side-effects `Controller` to orchestrate multiple services.179- Extract platform adapters (tiny protocols) for UI actions.180181## Output expectations182183When writing/refactoring:184185- Provide code in **diff-friendly chunks**, grouped by file.186- Prefer small, composable functions.187- Use extensions for organization (e.g., protocol conformances, grouping helpers).188- Name clearly: `FooViewModel`, `FooState`, `FooUseCase`, `FooRepository`, `FooController`, `FooRowBuilder`.189- Add tests for ViewModel state transitions and extracted pure logic.190191## Testing guidance192193Prefer **Swift Testing** (`import Testing`, `@Test`, `#expect`, `#require`) for new tests.194195- Test pure logic units directly (fast, deterministic).196- Test ViewModel state transitions (success/failure/cancellation) by injecting mocks.197198## Templates199200Copy/paste from:201202- `templates/ObservationViewModel.swift`203- `templates/CombineViewModel.swift`204- `templates/AppKitViewController.swift`205- `templates/ProtocolAdapters.swift`206- `templates/ServiceUseCaseControllerAndPureLogic.swift`207- `templates/ViewModelTests.swift` (Swift Testing)208209## Additional resources (load only if needed)210211- MVVM Overview: [references/mvvm-overview.md](references/mvvm-overview.md)212- Module Structure: [references/module-structure.md](references/module-structure.md)213- File Naming Conventions: [references/file-naming-conventions.md](references/file-naming-conventions.md)214- Where Things Go: [references/where-things-go.md](references/where-things-go.md)215- Common Patterns: [references/common-patterns.md](references/common-patterns.md)216- Adding New Features: [references/adding-new-features.md](references/adding-new-features.md)217- Integration Patterns: [references/integration-patterns.md](references/integration-patterns.md)218- Anti-Patterns: [references/anti-patterns.md](references/anti-patterns.md)219- Testing Considerations: [references/testing-considerations.md](references/testing-considerations.md)220- Services vs Feature Services: [references/services-vs-feature-services.md](references/services-vs-feature-services.md)221- Controller vs Coordinator: [references/controller-vs-coordinator.md](references/controller-vs-coordinator.md)222- State Management: [references/state-management.md](references/state-management.md)