MVVM in Swift — Complete Guide for SwiftUI Developers
What MVVM means in Swift in 2026, why it\'s still relevant with @Observable, and the production architecture indie iOS developers actually ship.
MVVM (Model-View-ViewModel) in Swift is an architectural pattern where the Model holds domain data, the View renders UI, and the ViewModel mediates between them. In SwiftUI 2026, the ViewModel is an @Observable class that owns view state and async work; the View reads it via @State or @Environment. MVVM remains the default architecture for SwiftUI iOS apps — TCA is the heavier alternative for teams who want it.
The Three Layers
MVVM splits an app into three distinct responsibilities:
- Model — plain Swift structs / classes representing domain data (Habit, Message, User). No SwiftUI imports.
- View — SwiftUI views that render UI. No business logic, no async work, no formatting beyond basic display.
- ViewModel — @Observable class that holds view state, formats data for display, and handles user intents (button taps, async calls). Imports Foundation, optionally Combine. No SwiftUI imports.
A Production ViewModel in SwiftUI 2026
Here's the @Observable pattern that replaces the old ObservableObject + @Published combo:
import Foundation
import Observation
@Observable
@MainActor
final class TaskListViewModel {
var tasks: [Task] = []
var isLoading = false
var errorMessage: String?
private let repository: TaskRepository
init(repository: TaskRepository) {
self.repository = repository
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
tasks = try await repository.fetchTasks()
} catch {
errorMessage = error.localizedDescription
}
}
func complete(_ task: Task) async {
try? await repository.markComplete(task.id)
await load()
}
}Binding the View
Views read @Observable state directly. Use @State if the view owns the ViewModel lifecycle; @Environment if it's shared.
struct TaskListView: View {
@State private var viewModel = TaskListViewModel(repository: .live)
var body: some View {
List(viewModel.tasks) { task in
TaskRow(task: task) {
Task { await viewModel.complete(task) }
}
}
.overlay {
if viewModel.isLoading { ProgressView() }
}
.task { await viewModel.load() }
}
}When to Skip the ViewModel
Don't over-architect. Skip the ViewModel for:
- Pure UI state — form fields, toggles, scroll position, sheet visibility
- Single-line view bodies — `Text(item.title)` doesn't need a ViewModel
- Tutorials / Apple sample code — they're intentionally compact
- Component-level state — keep it in @State on the component
- But: any view that does async work, formatting, or business logic → ViewModel
Why MVVM Over TCA
Both work; pick by team and complexity:
- MVVM is the default — smaller mental model, faster onboarding
- TCA adds testability + time-travel debugging at the cost of significant boilerplate
- For indie / solo developers: MVVM is the right call
- For 5+ engineer teams with strong functional background: TCA pays off
- You can mix — use MVVM by default and TCA in specific modules if needed
MVVM done right — out of the box.
The Swift Kit uses @Observable view models across every module, with consistent architecture and dependency injection patterns.
Frequently Asked Questions
What is MVVM in Swift?
Is MVVM still relevant in SwiftUI with @Observable?
When should I NOT use MVVM in SwiftUI?
What's the difference between MVVM and MVC in iOS?
Is MVVM or TCA better for SwiftUI?
Does The Swift Kit use MVVM?
Keep exploring
Ship your iOS app 10× faster
The Swift Kit gives you a production-ready SwiftUI boilerplate — design system, paywall, auth, AI, all pre-wired. $99 one-time.
Get The Swift Kit — $99One-time purchase · Lifetime updates · 14-day refund