swiftui-mvvm — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited swiftui-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 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.
View Layer → SwiftUI Views. Declarative UI only. Owns ViewModel via @State.
ViewModel Layer → @Observable @MainActor final class. Exposes ViewState<T>.
Repository Layer → Protocol-based data access. Hides data source details.
Service Layer → URLSession, persistence. Injected via protocol.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 testingDoes 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)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)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.
When: First encounter with a legacy SwiftUI 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] in the feature file and update refactoring/README.md progress tableWhen: Building a new feature screen from scratch. Apply enterprise patterns from the start so no refactoring is needed later.
references/networking.md)@Observable @MainActor final class with ViewState<T> (references/mvvm-observable.md)// MARK: - sections: Properties, Init, Actions, Computed Properties@State private var viewModel.task { await viewModel.load() }references/navigation.md)@Environment or @Injected (references/dependency-injection.md)references/testing.md)When: Modernizing existing code from Combine-based observation to @Observable.
Self._printChanges() to the View body — note current redraw triggersObservableObject conformance with @Observable macro@Published — plain var properties are auto-tracked@StateObject with @State in the owning View@ObservedObject with plain let (or @Bindable if $ bindings needed)@EnvironmentObject with @Environment(Type.self)@MainActor to the ViewModel class declarationSelf._printChanges() — confirm fewer/more specific redraw triggersSelf._printChanges() before committing<critical_rules> Whether generating new code or refactoring existing code, every output must be production-ready and PR-shippable — small, focused, and testable. ALWAYS:
@Observable @MainActor final classprivate(set) var for state properties modified only by the ViewModelViewState<T> enum for async data — never separate boolean flags.task { } for initial data loadingTask { } inside body, no business logicenum Route: Hashable for navigation// MARK: - sections: Properties, Init, Actions, Computed PropertiesFoundation (and domain modules) in ViewModels — never SwiftUIMyVM+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.<thought> analyzing its current state and redraw triggers..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).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.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.</critical_rules>
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<T> 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:
ObservableObject → @ObservableThis 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:
var stubbed* and var *CallCount tracking@MainActor when testing @MainActor ViewModelsawait fulfillment(of:) for async tests — NEVER wait(for:) (deadlocks)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:
$), 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.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.</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, 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/`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?"
| 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 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.