Use for hands-on UIKit MVVM questions: setting up Combine bindings between UIViewController and ViewModel (@Published + sink), extracting business logic from massive ViewControllers step by step, testing ViewModels with XCTestExpectation + Combine publishers, fixing [weak self] retain cycles in sink closures, migrating DispatchQueue/GCD completion handlers to Combine, or replacing reloadData() with DiffableDataSource. Also covers Coordinator navigation patterns, constructor injection, ViewSta...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add aibot88/sec_skill_store --skill uikit-mvvm --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Uikit Mvvm?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/aibot88-uikit-mvvm)More formats (shields.io, HTML) on the badges page.
---
name: uikit-mvvm
description: "Use for hands-on UIKit MVVM questions: setting up Combine bindings between UIViewController and ViewModel (@Published + sink), extracting business logic from massive ViewControllers step by step, testing ViewModels with XCTestExpectation + Combine publishers, fixing [weak self] retain cycles in sink closures, migrating DispatchQueue/GCD completion handlers to Combine, or replacing reloadData() with DiffableDataSource. Also covers Coordinator navigation patterns, constructor injection, ViewState enum, and incremental MVC-to-MVVM refactoring. Trigger on any UIKit architecture decision — even when the user doesn't mention 'MVVM' explicitly."
metadata:
version: 1.0.4
---
> **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.
# UIKit MVVM Architecture (iOS 13+)
Enterprise-grade UIKit MVVM architecture skill. Opinionated: prescribes Combine-bound ViewModels, Coordinator navigation, constructor injection via factories, ViewState enum, DiffableDataSource, and programmatic Auto Layout. Adopts a **production-first iterative refactoring** approach — every pattern is chosen for testability, reviewability, and safe incremental adoption in large teams. UIKit MVVM remains the dominant production architecture for large-scale iOS apps.
## Architecture Layers
```text
View/ViewController → UIViewController + UIView. Binds to ViewModel via Combine/closures/GCD. Renders state.
ViewModel Layer → Zero UIKit imports. Exposes ViewState<T>. Uses @Published (Combine) or closures (GCD).
Coordinator Layer → Manages navigation flow. Creates ViewModels + VCs. Owns UINavigationController.
Repository Layer → Protocol-based data access. Hides data source details.
Service Layer → URLSession, persistence. Injected via protocol. May use GCD or async/await.
```
## Quick Decision Trees
### "Should this ViewController have a ViewModel?"
```
Is there business logic, networking, or complex state?
├── YES → Create a ViewModel (see "Which binding mechanism" below for Combine vs closures)
└── NO → Is it a container/flow controller (tab bar, navigation)?
├── YES → Coordinator manages it, no ViewModel needed
└── NO → No ViewModel needed unless it simplifies testing
```
### "Which binding mechanism should I use?"
```text
Is this a legacy codebase with heavy GCD / completion handlers?
├── YES → Is the goal to extract ViewModels first (keep GCD)?
│ ├── YES → Closures / Bindable<T> + GCD in service layer
│ │ └── Upgrade to Combine later (see migration-patterns.md)
│ └── NO → Adopt Combine during extraction
│ └── @Published + sink (production standard)
└── NO → What is the minimum iOS target?
├── iOS 13+ → Combine: @Published + sink
│ └── One-shot events? → PassthroughSubject
└── < iOS 13 → Closures / Bindable<T> wrapper
```
### "Where do dependencies come from?"
```
ViewModel always receives dependencies via constructor:
init(service: NetworkServiceProtocol)
Who creates the ViewModel?
├── Coordinator (recommended)
│ └── Coordinator owns factory, creates VM with deps, passes to VC init
├── VC creates it (simpler apps)
│ └── VC receives deps via init, passes to VM init
└── DI Container (large apps, 20+ screens)
└── Container resolves protocols, coordinator pulls from container
```
### "How should I handle navigation?"
```
Is there more than one navigation flow (auth + main, tabs)?
├── YES → Coordinator pattern with parent/child hierarchy
│ └── AppCoordinator → AuthCoordinator / MainCoordinator → TabCoordinator
└── NO → Single Coordinator wrapping UINavigationController
└── ViewModel signals navigation via closures, never UIKit imports
```
## 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 MVC Codebase
**When:** First encounter with a legacy UIKit MVC 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]` and update the Progress table
8. Proceed through phases: Critical → ViewModel extraction → Coordinator → Combine bindings → DI
### Workflow: Create a New Screen
**When:** Building a new feature screen from scratch. Apply enterprise patterns from the start.
1. Define the data model and repository protocol (`references/testing.md` for mock pattern)
2. Create ViewModel: plain class with `private(set) var state: ViewState<T>` — zero UIKit imports (use `@Published` + Combine or closures per the binding mechanism decision tree)
3. Add `// MARK: -` sections: Properties, Init, Actions, Computed Properties
4. Create the ViewController with constructor injection: `init(viewModel: MyViewModel)`
5. Wire Combine bindings in `setupBindings()` called from `viewDidLoad`
6. Build UI programmatically with Auto Layout (`references/layout-approaches.md`)
7. Add Coordinator route and factory method (`references/coordinator-navigation.md`)
8. Create test file with mock repository (`references/testing.md`)
### Workflow: Extract ViewModel from Massive ViewController
**When:** Refactoring an existing MVC screen to MVVM incrementally.
1. Identify all state properties in the VC (data, loading flags, error state)
2. Create ViewModel class — move state properties to `@Published private(set) var`
3. Move business logic methods from VC to VM (networking, validation, formatting)
4. Add Combine `@Published` to VM state, `Set<AnyCancellable>` to VC
5. Replace direct state access in VC with `viewModel.$property.sink` bindings
6. Remove all `import UIKit` from ViewModel — compiler will flag violations
7. Write tests for the ViewModel (now possible since VM has no UIKit dependency)
8. Verify VC only does: bind, render, forward user actions to VM
### Workflow: Introduce Coordinators to Existing MVVM
**When:** Navigation is scattered across ViewControllers. Adding Coordinators incrementally.
1. Start with one flow (e.g., auth flow or a tab's navigation stack)
2. Create Coordinator protocol and AppCoordinator (`references/coordinator-navigation.md`)
3. Move VC creation from VCs/Storyboard segues to Coordinator's factory methods
4. Replace `performSegue` / `pushViewController` calls with ViewModel navigation closures
5. Wire Coordinator as `UINavigationControllerDelegate` for back-button cleanup
6. Add child coordinator lifecycle management (didFinish delegate pattern)
7. Test Coordinator with mock `UINavigationController` (`references/testing.md`)
## Code Generation Rules
<critical_rules>
Whether generating new code or refactoring existing code, every output must be **production-ready and PR-shippable** — small, focused, and testable. ALWAYS:
1. ViewModels import only `Foundation` and `Combine` — never `UIKit`
2. Use `@Published private(set) var` for state properties modified only by the ViewModel
3. Use `ViewState<T>` enum for async data — never separate boolean flags
4. Inject dependencies via constructor with protocol types
5. Bind in `viewDidLoad` using `setupBindings()` — store in `Set<AnyCancellable>`
6. Always `[weak self]` in `sink` closures when stored in `cancellables`
7. Always `.receive(on: DispatchQueue.main)` before UI updates in sink
8. Add `// MARK: -` sections: Properties, Init, Lifecycle, Bindings, Actions
9. Use programmatic Auto Layout — `translatesAutoresizingMaskIntoConstraints = false`
10. Keep every generated file ≤ 400 lines. Extract child VCs or ViewModel extensions when approaching that limit.
11. Before modifying a ViewController, output a brief `<thought>` analyzing its current dependencies and retain cycles.
</critical_rules>
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. Use `XCTestExpectation` + `sink` + `dropFirst()` for Combine publisher tests
4. Use `await fulfillment(of:)` for async tests — NEVER `wait(for:)` in async contexts. When you write async test code in a response, the **actual code example must call `await fulfillment(of: [expectation], timeout: 2.0)`**, not `wait(for: [expectation], timeout: 2.0)`. Mentioning the modern API only in a note while the code uses `wait(for:)` still hangs the test — it's the classic cooperative-pool deadlock. In XCTestCase this is a synchronous blocking call that cannot pump the queue, so the expectation never fulfills:
```swift
// ❌ WRONG — deadlocks in async test function
func test_load_updates_items() async {
let exp = expectation(description: "items updated")
let cancellable = sut.$items.dropFirst().sink { _ in exp.fulfill() }
await sut.load()
wait(for: [exp], timeout: 2.0) // cooperative-pool deadlock
_ = cancellable
}
// ✅ CORRECT — releases the cooperative thread via await
func test_load_updates_items() async {
let exp = expectation(description: "items updated")
let cancellable = sut.$items.dropFirst().sink { _ in exp.fulfill() }
await sut.load()
await fulfillment(of: [exp], timeout: 2.0)
_ = cancellable
}
```
5. Include memory leak detection with `addTeardownBlock { [weak sut] in XCTAssertNil(sut) }`
## Fallback Strategies & Loop Breakers
<fallback_strategies>
When refactoring legacy code, you may encounter stubborn Swift compiler errors. If you fail to fix the same error twice, break the loop:
1. **Combine Type Erasure:** If you get generic type mismatch errors with Combine `AnyPublisher`, append `.eraseToAnyPublisher()` to the pipeline or fall back to closures instead of fighting the type system. Why: Combine's generic types nest deeply (e.g., `Publishers.Map<Publishers.Filter<...>, Output>`), and type erasure is the standard solution.
2. **DiffableDataSource Generics:** If the compiler complains about `Hashable` conformance or type differences in `NSDiffableDataSourceSnapshot`, verify your `CellViewModel` uses a unique `UUID` instead of complex nested generic models. Why: DiffableDataSource compares items by hash — nested models with mutable state produce inconsistent hashes and silent data corruption.
3. **Revert and Restart:** If a ViewController refactor spirals into 50+ compiler errors, stop. Propose reverting the changes and breaking the problem into two smaller phases (e.g., extract networking first, then migrate state). Why: UIKit's implicit dependencies cascade — changing one property often breaks unrelated outlets, delegates, and data sources.
</fallback_strategies>
## 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 + Combine, NOT UIKit
□ ViewState — used for all async data, no separate isLoading/error booleans
□ Combine bindings — [weak self] in every sink, .receive(on: .main) before UI updates
□ DI — dependencies injected via protocol, not accessed via singletons
□ Coordinator — navigation handled by Coordinator, not by VC pushing other VCs
□ Memory management — deinit cancels Tasks, no retain cycles in closures
□ 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
```
## Companion Skills
> **Before generating async ViewModel or migrating completion handlers:** 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 (migrating or greenfield) | `skills/swift-concurrency/SKILL.md` | Migrating completion handlers, writing async ViewModel methods, actor-based state |
| `DispatchQueue`, `OperationQueue` (staying or auditing existing) | `skills/gcd-operations/SKILL.md` | Reviewing existing queue code, writing queue-based concurrency, thread-safe collections |
| Mixed (GCD stays, new code gets async/await) | Both skills | Apply GCD rules to existing code, concurrency rules to new code |
**If unclear, ask:** "Is the team migrating to Swift Concurrency or keeping GCD/OperationQueue?"
## References
| Reference | When to Read |
|-----------|-------------|
| `references/rules.md` | Do's and Don'ts quick reference: priority rules and critical anti-patterns |
| `references/binding-mechanisms.md` | Combine @Published + sink, closures, async/await, Input/Output pattern, decision matrix |
| `references/coordinator-navigation.md` | Coordinator protocol, hierarchy, memory management, back button handling, deep linking |
| `references/viewcontroller-lifecycle.md` | VC lifecycle, ViewState enum, DiffableDataSource, VC containment, keyboard handling |
| `references/dependency-injection.md` | Constructor injection, Factory pattern, Storyboard DI, Container/Resolver, @Injected wrapper |
| `references/layout-approaches.md` | Programmatic Auto Layout, UIStackView, XIBs, Storyboards, decision criteria |
| `references/testing.md` | Testing Combine publishers, async ViewModels, mocking, memory leak detection, Coordinator tests |
| `references/anti-patterns.md` | Code review detection checklist, severity-ranked UIKit MVVM violations |
| `references/migration-patterns.md` | MVC → MVVM, UIKit → SwiftUI, Combine adoption strategies |
| `references/refactoring-workflow.md` | `refactoring/` directory protocol, per-feature plans, PR sizing, phase ordering |
| `references/file-organization.md` | File size guidelines, ViewModel extension splits, child ViewControllers and subclassing views |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!