App Architecture: MVVM & Why It Fits SwiftUI

iOS Development — Architecture & Data

Chapter 1 · App Architecture: MVVM & Why It Fits SwiftUI

Fundamentals' own capstone, TaskFlow, worked — but its TaskStore quietly did two genuinely different jobs at once: holding raw task data, and deciding what a screen should display. This course opens by giving that split a real name, MVVM, and a deliberate structure.

What MVVM Actually Is

MVVM — Model-View-ViewModel — is a real, well-established UI architecture pattern originally developed at Microsoft by architects Ken Cooper and Ted Peters, and publicly announced by fellow Microsoft architect John Gossman on his blog in 2005, as a variant of Martin Fowler's own earlier Presentation Model pattern.

Model

Plain data — Task from Fundamentals, unchanged. No UI knowledge at all.

ViewModel

Holds state and logic for one screen — fetching, validating, transforming Models into what the View needs.

View

A real SwiftUI View — displays what the ViewModel exposes, forwards user actions back to it.

An Honest, Important Distinction
MVVM is not an official Apple-mandated architecture — Apple's own documentation and sample code have never formally required one specific pattern for SwiftUI apps. MVVM is a real, widely adopted community convention that happens to map unusually cleanly onto SwiftUI's own tools, covered next — not a rule enforced anywhere by the compiler or the framework itself.

Why It Fits SwiftUI So Naturally

MVVM's own classic problem, in older UI frameworks, was wiring a View up to a ViewModel's changes by hand. SwiftUI's real Observation framework (Fundamentals Chapter 6) removes that problem almost entirely — an @Observable class is a real, natural ViewModel, and SwiftUI's own view-diffing mechanism handles the "notify the View when something changes" job that MVVM traditionally needed a separate binding system for.

MVVM RoleReal SwiftUI Tool
ModelA plain struct — Identifiable/Codable as needed, no framework dependency
ViewModelAn @Observable class (iOS 17/Swift 5.9) — real, automatic change tracking, no manual @Published
ViewA real SwiftUI View struct, holding its ViewModel via @State or receiving it via @Bindable

Refactoring TaskFlow: A Real Before/After

Fundamentals' own TaskStore mixed a genuinely raw data array with screen-specific concerns — nothing was wrong with it for a 10-chapter capstone, but it doesn't scale cleanly once a real app grows past one screen's worth of logic.

Before — TaskStore.swift (Fundamentals, Chapter 10)
@Observable final class TaskStore { var tasks: [Task] = [ /* ... */ ] func add(title: String, priority: Int) { /* ... */ } func toggleDone(for task: Task) { /* ... */ } }

The real MVVM version keeps Task itself as a pure Model, and gives the task-list screen its own dedicated ViewModel — a real, meaningful rename, not just cosmetic, since it now explicitly owns this screen's own logic rather than being a single, all-purpose store:

After — TaskListViewModel.swift
@Observable final class TaskListViewModel { private(set) var tasks: [Task] = [ /* ... */ ] var incompleteCount: Int { tasks.filter { !$0.isDone }.count } func add(title: String, priority: Int) { tasks.append(Task(title: title, priority: priority)) } func toggleDone(for task: Task) { guard let index = tasks.firstIndex(where: { $0.id == task.id }) else { return } tasks[index].isDone.toggle() } }
A Real, Genuine Improvement, Not Just a Rename
incompleteCount is a real, new computed property — exactly the kind of view-specific derived state MVVM says belongs on the ViewModel, not scattered as ad hoc logic inside a View's own body. And private(set) var tasks is a real, meaningful access restriction: any View can read the task list, but only the ViewModel's own methods can change it — genuinely enforced by the compiler, not just a convention.

The View itself barely changes — it still holds the object via @State/@Bindable exactly as Fundamentals Chapter 6 covered — only the type name changes, reflecting what it now genuinely represents:

struct TaskListView: View { @Bindable var viewModel: TaskListViewModel var body: some View { List(viewModel.tasks) { task in Text(task.title) } .navigationTitle("\(viewModel.incompleteCount) remaining") } }
The Real Rule of Thumb
If a View's own body needs an if/switch to decide what data to show (not just how to lay it out), that's a real, genuine sign the logic belongs on the ViewModel instead — incompleteCount above is exactly that kind of decision, moved out of the View.

Hands-On Exercises

Exercise 1

Add a real computed property highPriorityTasks: [Task] to TaskListViewModel, filtering tasks to only those with priority == 3, and use it in TaskListView to show a separate Section("High Priority") above the main task list.

📄 View solution
Exercise 2

Explain, in your own words, what real problem private(set) var tasks prevents that a plain var tasks would allow, and why that matters once an app has more than one View sharing the same ViewModel.

📄 View solution
Exercise 3

Explain, in your own words, why MVVM being a "community convention rather than an Apple mandate" is a genuinely important distinction to understand before adopting it in a real project, rather than treating it as a rule the compiler or SwiftUI itself enforces.

📄 View solution

Chapter 1 Quick Reference

  • MVVM: real 2005 Microsoft origin (Cooper, Peters, announced by Gossman), a variant of Fowler's Presentation Model — a community convention, not an Apple mandate
  • Model = plain data, ViewModel = state/logic for one screen, View = a real SwiftUI View
  • @Observable (iOS 17/Swift 5.9) makes a plain class a natural, real ViewModel — no manual @Published wiring needed
  • private(set) var is a real, compiler-enforced way to let Views read but not directly mutate ViewModel state
  • A rule of thumb: view-specific decision logic (filtering, computed summaries) belongs on the ViewModel, not inside a View's own body