# Swiftui Mvvm

> 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 @Observab…

- **Type:** Skill
- **Install:** `agentstack add skill-rusel95-ios-agent-skills-swiftui-mvvm`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [rusel95](https://agentstack.voostack.com/s/rusel95)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [rusel95](https://github.com/rusel95)
- **Source:** https://github.com/rusel95/ios-agent-skills/tree/main/skills/swiftui-mvvm

## Install

```sh
agentstack add skill-rusel95-ios-agent-skills-swiftui-mvvm
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

> **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.
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.

1. Scan for anti-patterns using the detection checklist (`references/anti-patterns.md`)
2. Create `refactoring/` directory with per-feature plan files (`references/refactoring-workflow.md`)
3. Write each issue with **full description** (Location, Severity, Problem, Fix) — titles alone get forgotten
4. Categorize issues by severity: 🔴 Critical → 🟡 High → 🟢 Medium
5. Plan Phase 1 PR: fix critical safety issues only (≤200 lines per PR)
6. Execute one PR at a time. New findings go to `refactoring/discovered.md` with full descriptions, NOT into current PR
7. After completing each fix: mark the task `- [x]` in the feature file and update `refactoring/README.md` progress table
8. 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.

1. Define the data model and repository protocol (`references/networking.md`)
2. Create ViewModel: `@Observable @MainActor final class` with `ViewState` (`references/mvvm-observable.md`)
3. Add `// MARK: -` sections: Properties, Init, Actions, Computed Properties
4. Create the screen View with `@State private var viewModel`
5. Wire data loading via `.task { await viewModel.load() }`
6. Add navigation route to Router enum (`references/navigation.md`)
7. Register dependencies in `@Environment` or `@Injected` (`references/dependency-injection.md`)
8. Create test file with mock repository (`references/testing.md`)

### Workflow: Migrate ViewModel from ObservableObject

**When:** Modernizing existing code from Combine-based observation to @Observable.

1. Add `Self._printChanges()` to the View body — note current redraw triggers
2. Replace `ObservableObject` conformance with `@Observable` macro
3. Remove all `@Published` — plain `var` properties are auto-tracked
4. Replace `@StateObject` with `@State` in the owning View
5. Replace `@ObservedObject` with plain `let` (or `@Bindable` if `$` bindings needed)
6. Replace `@EnvironmentObject` with `@Environment(Type.self)`
7. Add `@MainActor` to the ViewModel class declaration
8. Verify with `Self._printChanges()` — confirm fewer/more specific redraw triggers
9. Run existing tests — all must pass
10. Remove `Self._printChanges()` before committing

## Code Generation Rules

Whether generating new code or refactoring existing code, every output must be **production-ready and PR-shippable** — small, focused, and testable. ALWAYS:

1. Mark ViewModels as `@Observable @MainActor final class`
2. Use `private(set) var` for state properties modified only by the ViewModel
3. Use `ViewState` enum for async data — never separate boolean flags
4. Inject dependencies via constructor with protocol types
5. Use `.task { }` for initial data loading
6. Keep View bodies pure — no `Task { }` inside body, no business logic
7. Use typed `enum Route: Hashable` for navigation
8. Add `// MARK: -` sections: Properties, Init, Actions, Computed Properties
9. Import only `Foundation` (and domain modules) in ViewModels — never `SwiftUI`
10. 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.
11. Before modifying a View or ViewModel, output a brief `` analyzing its current state and redraw triggers.
12. **`@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)`.
13. **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`.
14. **`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` 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:

1. Use protocol mocks with `var stubbed*` and `var *CallCount` tracking
2. Test through public interface, never test private methods
3. Mark test classes/structs `@MainActor` when testing `@MainActor` ViewModels
4. Use `await fulfillment(of:)` for async tests — NEVER `wait(for:)` (deadlocks)
5. Include memory leak detection with `addTeardownBlock { [weak sut] in XCTAssertNil(sut) }`

## Fallback Strategies & Loop Breakers

When refactoring legacy code, you may encounter stubborn Swift compiler errors. If you fail to fix the same error twice, break the loop:

1. **@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.
2. **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.
3. **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 |

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [rusel95](https://github.com/rusel95)
- **Source:** [rusel95/ios-agent-skills](https://github.com/rusel95/ios-agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-rusel95-ios-agent-skills-swiftui-mvvm
- Seller: https://agentstack.voostack.com/s/rusel95
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
