Skip to content

MVVM ​

Model-View-ViewModel places presentation state and user actions in a view model while the view declares how to render that state. The model represents domain data and services. MVVM is an architectural pattern, broader than a single object-level pattern.

iOS use case ​

An order screen loads one order, then shows a loading indicator, content, or an error. The SwiftUI view should not issue network requests or decide how errors become display text.

swift
import SwiftUI

struct Order {
    let number: String
}

protocol OrderLoading {
    func loadOrder() async throws -> Order
}

@MainActor
final class OrderViewModel: ObservableObject {
    enum Phase { case idle, loading, loaded(Order), failed(String) }

    @Published private(set) var phase: Phase = .idle
    private let loader: any OrderLoading

    init(loader: any OrderLoading) { self.loader = loader }

    func load() async {
        phase = .loading
        do { phase = .loaded(try await loader.loadOrder()) }
        catch { phase = .failed("Could not load this order") }
    }
}

struct OrderScreen: View {
    @StateObject var model: OrderViewModel

    var body: some View {
        Group {
            switch model.phase {
            case .idle, .loading: ProgressView()
            case .loaded(let order): Text("Order \(order.number)")
            case .failed(let message): Text(message)
            }
        }
        .task { await model.load() }
    }
}

The view renders phase; the model handles loading and maps failures to presentation state. Inject a fake OrderLoading in previews and tests.

Production decisions ​

Create OrderViewModel once at the screen's owning boundary. .task can run again when view identity changes; make load() idempotent or intentionally refresh. Cancellation, retry, and accessibility copy belong in the feature design. Keep domain rules in services or models rather than making the view model a large business layer.

Keep each layer's job visible ​

Order is the domain data. OrderLoading is a service boundary for obtaining it. OrderViewModel owns presentation state and converts success or failure into a Phase. OrderScreen switches over that phase and renders controls. Because the view does not instantiate a network client, the same screen can render a preview model and production model. The main-actor annotation keeps phase changes safe for UI observation.

From example to a production screen ​

The app or parent view constructs the model with a concrete OrderLoading implementation. @StateObject keeps the model stable across body updates; .task starts loading while the screen is visible. Add a Retry button that calls load() again after failure. Distinguish an empty result from an error if the domain allows no order. For pagination or live updates, expand the phase model deliberately rather than adding unrelated Booleans such as isLoading, hasError, and isEmpty that can contradict each other.

Failure modes and verification ​

Test each phase transition with fake loaders: delayed success, failure, cancellation, and repeated load. The simple .task can run again when view identity changes; decide whether that is a refresh or should be skipped. Keep business decisions, persistence, and navigation outside the view model when they belong to domain services or a coordinator. MVVM is a separation of responsibilities, not a requirement for one class per view.

All patterns

Released under the MIT License.