uikit-mvvm — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited uikit-mvvm (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.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.
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.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 testingIs 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> wrapperViewModel 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 containerIs 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 importsDefault 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.
When: First encounter with a legacy UIKit MVC codebase — the most common enterprise scenario.
references/anti-patterns.md)refactoring/ directory with per-feature plan files (references/refactoring-workflow.md)refactoring/discovered.md with full descriptions, NOT into current PR- [x] and update the Progress tableWhen: Building a new feature screen from scratch. Apply enterprise patterns from the start.
references/testing.md for mock pattern)private(set) var state: ViewState<T> — zero UIKit imports (use @Published + Combine or closures per the binding mechanism decision tree)// MARK: - sections: Properties, Init, Actions, Computed Propertiesinit(viewModel: MyViewModel)setupBindings() called from viewDidLoadreferences/layout-approaches.md)references/coordinator-navigation.md)references/testing.md)When: Refactoring an existing MVC screen to MVVM incrementally.
@Published private(set) var@Published to VM state, Set<AnyCancellable> to VCviewModel.$property.sink bindingsimport UIKit from ViewModel — compiler will flag violationsWhen: Navigation is scattered across ViewControllers. Adding Coordinators incrementally.
references/coordinator-navigation.md)performSegue / pushViewController calls with ViewModel navigation closuresUINavigationControllerDelegate for back-button cleanupUINavigationController (references/testing.md)<critical_rules> Whether generating new code or refactoring existing code, every output must be production-ready and PR-shippable — small, focused, and testable. ALWAYS:
Foundation and Combine — never UIKit@Published private(set) var for state properties modified only by the ViewModelViewState<T> enum for async data — never separate boolean flagsviewDidLoad using setupBindings() — store in Set<AnyCancellable>[weak self] in sink closures when stored in cancellables.receive(on: DispatchQueue.main) before UI updates in sink// MARK: - sections: Properties, Init, Lifecycle, Bindings, ActionstranslatesAutoresizingMaskIntoConstraints = false<thought> analyzing its current dependencies and retain cycles.</critical_rules>
When generating tests, ALWAYS:
var stubbed* and var *CallCount trackingXCTestExpectation + sink + dropFirst() for Combine publisher testsawait 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:// ❌ 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
}addTeardownBlock { [weak sut] in XCTAssertNil(sut) }<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:
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.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.</fallback_strategies>
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 loggedBefore 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?"
| 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 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.