Uikit mvvm
Production-tested iOS Agent Skills for Claude Code, Codex, and 40+ AI coding tools. 8 enterprise-grade skills covering SwiftUI MVVM, UIKit MVVM, VIPER, TCA, Swift Concurrency, GCD, Testing, and Security Audit.
npx -y skills add rusel95/ios-agent-skills --skill uikit-mvvmAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
14.7 KB, as published. Nobody here has run it
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
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?"
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.
- Scan for anti-patterns using the detection checklist (
references/anti-patterns.md) - Create
refactoring/directory with per-feature plan files (references/refactoring-workflow.md) - Write each issue with full description (Location, Severity, Problem, Fix) — titles alone get forgotten
- Categorize issues by severity: 🔴 Critical → 🟡 High → 🟢 Medium
- Plan Phase 1 PR: fix critical safety issues only (≤200 lines per PR)
- Execute one PR at a time. New findings go to
refactoring/discovered.mdwith full descriptions, NOT into current PR - After completing each fix: mark the task
- [x]and update the Progress table - 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.
- Define the data model and repository protocol (
references/testing.mdfor mock pattern) - 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) - Add
// MARK: -sections: Properties, Init, Actions, Computed Properties - Create the ViewController with constructor injection:
init(viewModel: MyViewModel) - Wire Combine bindings in
setupBindings()called fromviewDidLoad - Build UI programmatically with Auto Layout (
references/layout-approaches.md) - Add Coordinator route and factory method (
references/coordinator-navigation.md) - Create test file with mock repository (
references/testing.md)
Workflow: Extract ViewModel from Massive ViewController
When: Refactoring an existing MVC screen to MVVM incrementally.
- Identify all state properties in the VC (data, loading flags, error state)
- Create ViewModel class — move state properties to
@Published private(set) var - Move business logic methods from VC to VM (networking, validation, formatting)
- Add Combine
@Publishedto VM state,Set<AnyCancellable>to VC - Replace direct state access in VC with
viewModel.$property.sinkbindings - Remove all
import UIKitfrom ViewModel — compiler will flag violations - Write tests for the ViewModel (now possible since VM has no UIKit dependency)
- 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.
- Start with one flow (e.g., auth flow or a tab's navigation stack)
- Create Coordinator protocol and AppCoordinator (
references/coordinator-navigation.md) - Move VC creation from VCs/Storyboard segues to Coordinator's factory methods
- Replace
performSegue/pushViewControllercalls with ViewModel navigation closures - Wire Coordinator as
UINavigationControllerDelegatefor back-button cleanup - Add child coordinator lifecycle management (didFinish delegate pattern)
- 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:
- ViewModels import only
FoundationandCombine— neverUIKit - Use
@Published private(set) varfor state properties modified only by the ViewModel - Use
ViewState<T>enum for async data — never separate boolean flags - Inject dependencies via constructor with protocol types
- Bind in
viewDidLoadusingsetupBindings()— store inSet<AnyCancellable> - Always
[weak self]insinkclosures when stored incancellables - Always
.receive(on: DispatchQueue.main)before UI updates in sink - Add
// MARK: -sections: Properties, Init, Lifecycle, Bindings, Actions - Use programmatic Auto Layout —
translatesAutoresizingMaskIntoConstraints = false - Keep every generated file ≤ 400 lines. Extract child VCs or ViewModel extensions when approaching that limit.
- Before modifying a ViewController, output a brief
<thought>analyzing its current dependencies and retain cycles. </critical_rules>
When generating tests, ALWAYS:
- Use protocol mocks with
var stubbed*andvar *CallCounttracking - Test through public interface, never test private methods
- Use
XCTestExpectation+sink+dropFirst()for Combine publisher tests - Use
await fulfillment(of:)for async tests — NEVERwait(for:)in async contexts. When you write async test code in a response, the actual code example must callawait fulfillment(of: [expectation], timeout: 2.0), notwait(for: [expectation], timeout: 2.0). Mentioning the modern API only in a note while the code useswait(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:
// ❌ 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
}
- 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:
- 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. - DiffableDataSource Generics: If the compiler complains about
Hashableconformance or type differences inNSDiffableDataSourceSnapshot, verify yourCellViewModeluses a uniqueUUIDinstead of complex nested generic models. Why: DiffableDataSource compares items by hash — nested models with mutable state produce inconsistent hashes and silent data corruption. - 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 |