MVVM, Clean Architecture and VIPER
The three names you will meet in job specs and legacy codebases — what each actually prescribes, what it costs, and how to choose without cargo-culting.
By the end you will be able to
- Describe the roles in MVVM, Clean Architecture and VIPER and what talks to what
- Implement a use-case-with-repository slice and say which layers it demonstrates
- Choose an architecture for a given team and app — and defend the choice
Three iOS teams describe their architecture as "MVVM", "Clean" and "VIPER". What do all three actually have in common underneath the names?
MVVM — the default dose
Model–View–ViewModel: the view binds to a view model that holds presentation state and logic; the view model talks to model/services. You have been building this since the Observation lesson without the label:
View (SwiftUI) ──binds──▶ ViewModel (@Observable) ──uses──▶ Services / Model
@Observable
final class SearchViewModel { // the VM: state + presentation logic
private(set) var results: [City] = []
var query = ""
private let searcher: CitySearching // injected seam (DIP, as ever)
init(searcher: CitySearching) { self.searcher = searcher }
func search() async {
results = await searcher.cities(matching: query)
}
}
struct SearchView: View { // the View: renders, forwards intent
@State private var viewModel: SearchViewModel
var body: some View {
List(viewModel.results) { CityRow(city: $0) }
.searchable(text: $viewModel.query)
.task(id: viewModel.query) { await viewModel.search() }
}
}
What it buys: logic is testable without rendering; views stay boring; one new role beyond "view", so the whole team understands it by lunch.
What it doesn't: MVVM says nothing about what lives behind the view model. In small apps that's fine. In big ones, view models quietly swallow networking, caching and business rules — the 900-line god object returns wearing a badge that says "VM". MVVM is a screen-level pattern, not a whole-app answer.
Clean Architecture — an explicit core
Clean Architecture (Robert Martin's formulation, widely adapted for iOS) adds what MVVM leaves unsaid: a domain core that owns business rules, with all dependencies pointing inward.
┌──────────────────────────────────────────────┐
│ UI (SwiftUI views, view models) │ outer
│ ┌────────────────────────────────────────┐ │
│ │ Use cases (application actions) │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Entities (business types+rules) │ │ │ inner
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ Infrastructure (network, DB) — conforms in │
└──────────────────────────────────────────────┘
The Dependency Rule: references point INWARD only.
- Entities — business types and their rules (
Order, and what makes one refundable). Pure Swift, no imports. - Use cases — one application action each:
PlaceOrder,RefundOrder. They orchestrate entities and talk to the outside through repository protocols they own (Dependency Inversion, formalised as a layer). - Interface adapters — view models mapping domain results to screen state.
- Infrastructure — URLSession clients, SwiftData stores, conforming to the inner protocols from outside.
The payoff is concentrated in one property: the core compiles and tests with no framework imports at all. Business rules outlive UI frameworks — teams that survived the UIKit→SwiftUI migration with a clean core rewrote views, not rules.
Read the dependency arrows: RefundOrder knows OrderRepository — a protocol it owns — and has never heard of InMemoryOrders, URLSession, or SwiftData. Swap the in-memory store for a real backend and the use case is untouched. The 30-day rule lives on the entity, tested by constructing a struct. Every test double you will ever need is "another infrastructure".
The cost: every feature touches more files (entity, use case, protocol, conformance, VM), and simple CRUD screens gain nothing from the ceremony. Clean earns its keep when the domain is genuinely complex — pricing rules, permissions, offline merge — and when multiple frontends (app, widget, watch) must share one set of rules.
VIPER — the maximal prescription
VIPER (View, Interactor, Presenter, Entity, Router) came out of large UIKit codebases — formulated at Mutual Mobile and published in objc.io's 2014 architecture issue, famously adopted at scale — as a reaction to Massive View Controller. It fixes five roles per screen:
| Role | Owns | In the refund example |
|---|---|---|
| View | pixels and user events, passive | shows the result label |
| Interactor | the business operation | RefundOrder — same idea as the use case |
| Presenter | mediates: formats for the view, routes intents | turns the outcome into label text |
| Entity | the data types | Order |
| Router | navigation, screen assembly | builds the module, pushes the next one |
Each boundary is a protocol, so a screen ("module") is typically five types plus five protocols, wired by the router. That regularity is the point: at 40 engineers, every screen having the same shape means anyone can open any module and know where everything is, and merge conflicts drop because roles rarely collide.
Why it fights SwiftUI: the Presenter exists to push formatted state into a passive view — but SwiftUI views pull from observed state; the binding is the presenter's job, done by the framework. The Router assembles view controllers and pushes them — but NavigationStack(path:) made navigation a value you set, not an object that imperatively pushes. Porting VIPER into SwiftUI means re-implementing what the framework gives you, which is why you will find VIPER thriving in large UIKit codebases (where you may well maintain it — banks, marketplaces, super-apps) and almost never chosen for new SwiftUI work.
Choosing — the part interviews actually probe
| MVVM | Clean | VIPER | |
|---|---|---|---|
| Roles per screen | 2 | 3–5 | 5 + protocols |
| Domain isolated from frameworks | if you're disciplined | by construction | yes |
| Ceremony per feature | low | medium | high |
| SwiftUI fit | natural | good (Clean core + MVVM shell) | poor |
| Sweet spot | most apps, small teams | complex domain, multiple frontends | huge UIKit teams, uniformity above all |
The honest decision procedure:
- Start with MVVM-shaped separation — it is the SwiftUI grain, and every later architecture contains it.
- Extract a Clean-style core when the domain earns it — the moment business rules are real (money, permissions, sync), move them into framework-free entities and use cases. You do not need the full vocabulary on day one; you need the dependency rule.
- Adopt VIPER only to join it — learn it to be effective in codebases that have it; do not bring it to new SwiftUI projects.
And the meta-rule from the SOLID lesson applies to whole architectures: consistency beats purity. Watch one bug cross a mixed codebase: it starts on a VIPER screen (does the fix go in the Interactor?), surfaces on an MVVM screen (or the ViewModel?), and the data layer under both is half-migrated (the new repository, or the old manager?). Three philosophies means "where does the fix go" has three answers, and every change starts with archaeology. A uniformly middling-MVVM codebase answers it once — which is why architecture migrations count as migrations, with plans, not as drive-by refactors.
Your team of four is building a new SwiftUI app: subscription pricing with regional rules, offline entitlements, plus app, widget and watch targets sharing that logic. What does the decision procedure give?
Build a use case behind a repository
Implement the Clean slice for article bookmarking: the use case enforces the "max 3 bookmarks on the free plan" rule, talking only to its repository protocol.
Same feature, three vocabularies
Cement the mapping: given a role description, print which component owns it in each architecture. Fill in the table for the "show refund result" feature.
A junior teammate asks: "The job spec says VIPER, our codebase says MVVM, and a conference talk said both are obsolete. What should I actually learn?" Write your answer in five sentences — it should leave them knowing what transfers between all architectures (the two moves), and what is just vocabulary.
You can now:
- Draw MVVM, Clean and VIPER and name what talks to what
- Implement an entity/use-case/repository slice with the dependency rule intact
- Explain why VIPER's Presenter and Router fight SwiftUI
- Choose a dose of architecture for a given team — and defend consistency over purity
Next up: testing — the practice all this structure was buying.