D DazzleCODE
CHEATSHEET · v1.0

The senior iOS
architecture cheatsheet

MVVM + Coordinator + Clean Architecture. Production-proven at enterprise scale. One page. No fluff.

by Alexandru Rares Zehan
14+ years native Apple
dazzlecode.com

01 · Layers The stack, top to bottom

View
SwiftUI / UIKit. Dumb. No logic.
ViewModel
State + input events. Orchestrates.
Use Case
Business rules. Pure, testable.
Repository
Data source of truth. Caches.
Service
Network / Keychain / Disk.

Dependencies flow downward only. Lower layers never know about upper ones. This is how you make code testable, swappable, and scalable.

02 · Patterns Nine core patterns, in order of impact

01 · MVVM

Model–View–ViewModel

UI binds to @Published state. ViewModel owns state and translates events into actions. Never import UIKit inside a ViewModel.

02 · Coordinator

Navigation as its own object

Pulls navigation out of View and ViewModel. One coordinator per flow. Handles present, push, deep links, dismiss. Views and ViewModels stay pure.

03 · Clean Architecture

Use Cases are king

One use case per business action: LoginUser, FetchHomeFeed. They compose repositories. ViewModels call use cases — never repositories directly.

04 · Dependency Injection

Constructor injection, always

Pass dependencies through init. Avoid singletons and service locators. If DI feels heavy, that's a code smell — the object probably does too much.

05 · Protocols

Abstract the collaborator, not the concept

Protocols exist to enable swap (fake/mock/alt). Don't protocolize everything — only what crosses a layer boundary or needs a test double.

06 · State

Single source of truth

One @Published state enum per screen: idle / loading / loaded / error. No duplicate booleans. Derive everything from state.

07 · Concurrency

Swift Concurrency over Combine

Use async/await + AsyncSequence for new code. Mark UI types @MainActor. Reach for Combine only when existing code already uses it.

08 · Persistence

Repository hides the storage

ViewModels never know if data came from network, cache, Core Data, or SwiftData. Repository decides. Swap storage without touching the UI layer.

09 · Tests

ViewModels and Use Cases first

Test state transitions and business rules. Skip pure View tests — snapshots cover them. Mock services at the protocol boundary with stubs, not frameworks.

03 · Snippet A proper ViewModel in <30 lines

@MainActor
final class HomeViewModel: ObservableObject {

    enum State { case idle, loading, loaded([Item]), error(String) }

    @Published private(set) var state: State = .idle

    private let fetchFeed: FetchHomeFeedUseCase
    private let analytics: AnalyticsService

    init(fetchFeed: FetchHomeFeedUseCase, analytics: AnalyticsService) {
        self.fetchFeed = fetchFeed
        self.analytics = analytics
    }

    func onAppear() async {
        state = .loading
        analytics.track(.homeOpened)
        do {
            let items = try await fetchFeed.run()
            state = .loaded(items)
        } catch {
            state = .error(error.localizedDescription)
        }
    }
}

04 · Red flags When you've gone wrong

smell

2,000-line ViewController

You skipped MVVM. Extract state to a ViewModel. Extract navigation to a Coordinator. Extract networking to a Service.

smell

.shared everywhere

Singletons make tests impossible. Inject through init. If bootstrapping gets noisy, add a composition root (AppDependencies).

smell

ViewModel imports UIKit

Leaking UI into state. Pass events in, publish state out. Never UINavigationController in a ViewModel.

smell

Parallel booleans

isLoading, hasError, isEmpty, didLoad — four booleans encoding one state. Collapse into a single enum.

smell

Repositories returning JSON

Repos must return domain types, not transport types. Never pass [String: Any] up to a ViewModel. Decode at the boundary.

smell

No test coverage on ViewModels

If you can't test state transitions without launching the app, your architecture is wrong — not your tests.