100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Swift

MVVM in SwiftUI

Understand how the Model-View-ViewModel pattern maps onto SwiftUI's declarative views, when it earns its complexity, and how it differs from plain @State-driven views.

Architecture PatternsIntermediate10 min readJul 8, 2026
Analogies

MVVM in SwiftUI

Model-View-ViewModel separates an app into three responsibilities: Models hold plain data and business rules independent of any UI framework, Views render state and forward user intent, and ViewModels sit between them, exposing UI-ready state and performing the logic (validation, networking orchestration, formatting) that a view shouldn't own directly. SwiftUI's View struct already behaves somewhat like a lightweight view-model for trivial screens through @State, which raises a real design question: MVVM earns its keep on screens with nontrivial logic, async work, or business rules worth unit testing in isolation from any UI — not on every single view in an app.

🏏

Cricket analogy: Like separating a match's raw data (scorecard, Model), the broadcast graphics (View), and the analyst translating stats into insights for commentary (ViewModel), MVVM only earns its place when there's real analysis logic worth isolating, not for every routine over.

Where the Line Sits

A view that just displays a Bool toggle or formats a static string doesn't need a ViewModel — that's exactly what @State and computed properties on the View struct are for. A view that calls a network service, validates form input against multiple rules, coordinates several async operations, or has business logic that a QA team would write test cases against, benefits from a ViewModel: the logic becomes testable without instantiating any SwiftUI rendering machinery at all.

🏏

Cricket analogy: Like a scoreboard that just flips a boolean for 'rain delay' needing no analyst (just @State), but a screen computing net run rate across a tournament with multiple tiebreak rules benefits from a dedicated ViewModel worth unit testing.

swift
@MainActor
@Observable
final class LoginViewModel {
    var email = ""
    var password = ""
    var errorMessage: String?
    private(set) var isLoading = false

    private let authService: AuthServicing

    init(authService: AuthServicing) {
        self.authService = authService
    }

    var canSubmit: Bool {
        email.contains("@") && password.count >= 8
    }

    func signIn() async {
        guard canSubmit else { return }
        isLoading = true
        errorMessage = nil
        do {
            try await authService.signIn(email: email, password: password)
        } catch {
            errorMessage = "Sign in failed. Check your credentials."
        }
        isLoading = false
    }
}

struct LoginView: View {
    @State private var viewModel: LoginViewModel

    init(authService: AuthServicing) {
        _viewModel = State(initialValue: LoginViewModel(authService: authService))
    }

    var body: some View {
        Form {
            TextField("Email", text: $viewModel.email)
            SecureField("Password", text: $viewModel.password)
            if let error = viewModel.errorMessage {
                Text(error).foregroundStyle(.red)
            }
            Button("Sign In") {
                Task { await viewModel.signIn() }
            }
            .disabled(!viewModel.canSubmit || viewModel.isLoading)
        }
    }
}

Testability Is the Real Payoff

The core benefit of MVVM isn't architectural purity for its own sake — it's that canSubmit, signIn(), and error-handling logic in the example above can be unit tested by instantiating LoginViewModel with a mock AuthServicing implementation, asserting on its published state, with zero SwiftUI rendering involved. Without extracting that logic, it would live inside the View's body and button actions, where it's much harder to exercise without a UI test harness.

🏏

Cricket analogy: Like testing a run-rate calculator by feeding it a mock match feed instead of waiting for a real live game, LoginViewModel's canSubmit and signIn() logic gets unit tested with a mock AuthServicing implementation, no scoreboard UI needed.

A common anti-pattern is a 'ViewModel' that does nothing but hold @State-equivalent properties with no real logic — this adds indirection and boilerplate without buying any testability or reuse. If a view's entire ViewModel is just properties with no computed logic or async work, plain @State on the View is simpler and just as correct.

Because SwiftUI already provides a reactive, declarative rendering layer, SwiftUI MVVM ViewModels rarely need the delegate/callback machinery classic UIKit MVVM required — @Observable properties (or @Published on ObservableObject) are enough for the view to react automatically whenever relevant state changes.

  • MVVM separates Models (data/business rules), Views (rendering/intent forwarding), and ViewModels (UI-ready state + logic).
  • Not every SwiftUI view needs a ViewModel — trivial, display-only views are well served by plain @State.
  • ViewModels earn their complexity on screens with nontrivial logic, async orchestration, or validation worth unit testing.
  • The main payoff of MVVM is testing business logic in isolation, without instantiating SwiftUI's rendering layer.
  • A ViewModel that's just a bag of properties with no real logic adds indirection without benefit.
  • @Observable (or @Published/ObservableObject) lets SwiftUI views react automatically to ViewModel state changes, without manual delegate wiring.

Practice what you learned

Was this page helpful?

Topics covered

#Swift#IOSWithSwiftUIStudyNotes#MobileDevelopment#MVVMInSwiftUI#MVVM#SwiftUI#Where#Line#StudyNotes#SkillVeris