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.
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.