App architecture · advanced
MVVM, Clean Architecture and VIPER
The three architecture names you will meet in job adverts and in old codebases. What each one actually asks you to do, what it costs you, and how to pick one for reasons you can defend.
By the end you will be able to
- Describe the roles in MVVM, Clean Architecture and VIPER, and say what talks to what
- Implement a use case with a repository behind it, and name the 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 smallest dose
These three architectures are three doses of the same two ideas. A dose is how much of something you take: the same medicine, in a larger or smaller amount. MVVM is the smallest one.
Model–View–ViewModel. The view binds to a view model. Binding means the view reads the view model's properties and redraws itself whenever one of them changes.
The view model holds the screen's presentation state: the values the screen currently shows, such as the list of results and the text in the search field. It also holds the logic that produces those values, and it calls the services that fetch and store data.
Traffic runs the other way too. The view forwards intents to the view model. An intent is something the user wants to do — "search for this", "refund this order".
You have been writing this shape since U2-02, the Observation lesson. Nobody attached the name MVVM to it.
@Observable
final class SearchViewModel { // holds what the screen shows, and the logic that fills it
private(set) var results: [City] = []
var query = ""
private let searcher: any CitySearching // a protocol, passed in from outside: Dependency Inversion
init(searcher: any CitySearching) { self.searcher = searcher }
func search() async {
results = await searcher.cities(matching: query)
}
}
struct SearchView: View { // draws the results, forwards what the user does
@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() }
}
}
That CitySearching protocol is a seam: D1-02's word for a place you plan in advance, so behaviour can be swapped without editing the file that uses it. The app passes in the real searcher. A test passes in one that returns three cities immediately.
What MVVM buys you. Three things. The logic can be tested without drawing anything on screen. Views stay simple, because all they do is display values and forward taps. And it adds exactly one new role to the two everybody already knows, so a new team member can learn it on their first day.
What MVVM does not do. It says nothing about what sits behind the view model. In a small app that does not matter. In a large one, here is how a view model becomes the 900-line type from D1-02. Follow the sequence:
- The first version of
SearchViewModelcalls one service and holds one array. It is 30 lines. - The screen has to work offline, so an engineer adds a cache to the view model. It is the only type in the feature that already holds the results, so it is the obvious place.
- The search rules grow: recent searches first, promoted cities pinned to the top, some cities hidden in some countries. Those rules go inside
search(), because that is where the results are built. - Analytics, paging and retry-on-failure arrive later, and each one lands in the same type for the same reason.
SearchViewModelis now 900 lines. Four teams edit it, so their pull requests collide. No test can create one without a network client and a cache attached.
That is the 900-line view model from D1-02, reached one reasonable decision at a time. Engineers call a type in that state a god object: one type that has taken on everybody's work. MVVM is a pattern for one screen. It is not an answer for a whole app.
Clean Architecture — an explicit core
Clean Architecture is Robert C. Martin's arrangement of ideas that were already in circulation, and iOS teams adapt it freely. It supplies the thing MVVM leaves unsaid: a domain core. Domain means the subject your app is about — orders and refunds in a shop, transfers and balances in a bank. The core is the set of types holding those rules, and every dependency in the codebase is arranged to point towards it.
Read the figure from the outside in. Each ring may know about the ring inside it, and never the other way round. The view knows the use case. The use case knows the entity. The entity knows nobody. The orange arrow, labelled dependencies point inward, stands for every reference in the codebase: every import, every property type, every function parameter.
The dashed strip at the bottom marked Infrastructure looks like it breaks that rule. Networking code sits outside the rings, yet the app clearly has to reach a network at some point. The rule survives because of one move, and it is Dependency Inversion from D1-02. The inner layers declare the protocols. The infrastructure types conform to those protocols from outside, which is what the teal arrow labelled conforms to inner ports means. So the URLSession client depends on the core, and the core has still never heard of URLSession.
- Entities — the business types, and the rules that come with them.
Orderis an entity, and "an order can be refunded within 30 days" is one of its rules. These are plain Swift types: no UI framework, no networking library, no database. - Use cases — one type per action the app can perform:
PlaceOrder,RefundOrder. A use case applies the entity rules and reaches the outside world through repository protocols that it declares itself. A repository is a protocol for fetching and storing things —find(_:),save(_:)— that says nothing about where they are kept. This is Dependency Inversion again, this time made into a layer of the architecture. - Interface adapters — the translating layer, where view models live. They turn what a use case returns into the exact values a screen displays, and turn taps into calls on use cases.
- Infrastructure — the code that talks to the world: URLSession clients, SwiftData stores, keychain wrappers. Each one conforms to a protocol that an inner layer declared.
The payoff fits in one sentence: the core compiles, and its tests run, with no framework imported at all. That matters because UI frameworks get replaced and business rules do not. Teams that moved an app from UIKit to SwiftUI with a core like this rewrote their views and left their rules alone.
Here is one slice of that, small enough to run. Watch which type knows about which.
Check who knows whom in that code. RefundOrder knows OrderRepository, the protocol it declares itself. It has never heard of InMemoryOrders, of URLSession, or of SwiftData. Replace the in-memory store with a real backend and not one line of RefundOrder changes.
The 30-day rule lives on Order, so testing it means creating a struct and reading one property — no network, no database, no screen. And every test double you will ever need — a stand-in type you write so that a test can run without the real thing — is just one more type conforming to OrderRepository.
What Clean Architecture costs. Every feature now touches five files instead of one: the entity, the use case, the repository protocol, the type that conforms to it, and the view model. A screen that only lists rows from a server and lets you edit one of them gains nothing from that. Clean earns its cost in two situations. The first is a genuinely complicated domain: pricing rules, permissions, merging changes that were made offline. The second is several apps sharing one set of rules — a phone app, a widget and a watch app that must all agree on what a customer is allowed to do.
VIPER — the largest dose
VIPER stands for View, Interactor, Presenter, Entity, Router. It came out of large UIKit codebases, as a reaction to Massive View Controller. That was the name iOS engineers gave to a UIViewController of several thousand lines: every job on the screen had been written inside the one class. VIPER was formulated at Mutual Mobile and published in objc.io's 2014 architecture issue, and it was famously adopted at scale. It fixes five roles per screen:
| Role | Owns | In the refund example |
|---|---|---|
| View | what is drawn, and the user's taps. It decides nothing | shows the result label |
| Interactor | the business operation | RefundOrder — the same idea as a use case |
| Presenter | the middle: formats values for the view, passes the user's actions on | turns the outcome into label text |
| Entity | the data types | Order |
| Router | navigation, and building the screen's types | assembles the module, pushes the next screen |
Every connection between two of those roles is a protocol. VIPER calls one screen's set of types a module, and a module is typically five types plus five protocols, assembled by the router. All that repetition is the point. On a team of forty engineers, every screen has the same shape, so anyone can open a screen they have never seen and know which file to open. Two engineers working on the same screen usually touch different roles, so their changes rarely collide in the same file.
Why VIPER does not fit SwiftUI. Two of its five roles do a job that SwiftUI already does for you.
The Presenter exists to push formatted values into a view that does nothing on its own. SwiftUI works the other way round: a view reads the state it needs, and redraws itself when that state changes. The framework does the Presenter's job.
The Router exists to build screens and push them onto a navigation stack. NavigationStack(path:) turned navigation into a value you set — you append a route to an array and the screen appears, which is what D1-04 builds. There is nothing left for an object to push.
So putting VIPER into a SwiftUI app means writing by hand what the framework hands you for free. That is why VIPER is almost never chosen for new SwiftUI work. It lives on in large UIKit codebases: banks, marketplaces, apps that bundle many products together. You may well be paid to maintain one of them, which is the reason to learn it.
The cast, in plain words
Three architectures use eleven role names between them. Underneath, there are only six jobs. One picture holds all six: an airline. Learn which job each name refers to once, and the names stop being confusing — because every architecture above is only a different decision about which jobs get their own dedicated staff.
- View — the cabin. Everything the passenger sees and touches: the seats, the screens, the call button. The cabin never flies the aircraft. It shows what is happening and passes requests along. All three architectures agree about this role.
- ViewModel / Presenter — the cabin crew. They turn what is happening at the front of the aircraft into announcements a passenger understands ("we have begun our descent"), and they carry passengers' requests the other way. That is presentation state and formatting. The crew never fly. MVVM's crew is allowed to make small decisions itself. VIPER's Presenter is stricter: announce and relay, nothing more.
- Model / Entity — the aircraft and the rules of flight. What is true on every route, whoever is flying: the aircraft's limits, its weight and balance. In code, that is the business types and the rules they must always satisfy —
Order, and the rule that decides whether one can be refunded. - Use case / Interactor — the flight deck. One type flies one route.
RefundOrderis a single flight plan, carried out from start to finish. The flight deck applies the rules and asks for services; who supplies them is not its concern. - Repository (the port) — the ground-handling contract. The flight deck orders "fuel, 8 tonnes" through a standard contract, and does not care which company's truck arrives. In code: a protocol that the core declares, which infrastructure conforms to from outside. Port is the word D1-02 gives to exactly that kind of protocol.
- Router — air traffic control. It decides where the aircraft goes next. No pilot clears their own next leg. In code: navigation, plus building the screen's types — the job that
NavigationStack(path:)largely took over inside SwiftUI.
The three doses in one line: MVVM staffs the cabin. Clean adds the flight deck and the rulebook. VIPER staffs every seat, control tower included.
Choosing — the part interviews actually probe
| MVVM | Clean | VIPER | |
|---|---|---|---|
| Roles per screen | 2 | 3–5 | 5 + protocols |
| Business rules free of frameworks | only if you are disciplined | built in | yes |
| Extra code per feature | low | medium | high |
| Fit with SwiftUI | natural | good — Clean core, MVVM screens | poor |
| Best fit | most apps, small teams | complicated rules, several apps sharing them | large UIKit teams that value uniformity above all |
How to choose, in three steps:
- Start with MVVM-shaped separation. SwiftUI is built to work this way, and every larger architecture contains MVVM inside it anyway.
- Pull out a Clean-style core when the rules earn it. As soon as the business rules are real — money, permissions, syncing — move them into entities and use cases that import no framework. You do not need the full vocabulary on day one. You need the dependency rule: the core depends on nothing outside itself.
- Learn VIPER in order to join a team that already uses it. Know it well enough to be useful in a codebase built on it. Do not start a new SwiftUI project with it.
One more rule outranks all three: consistency beats purity. A codebase where every screen is built the same average way is cheaper to work in than one where each screen was built the best way for itself. Here is what a mixed codebase does to a single bug report. A customer says a refund shows the wrong amount, and the ticket goes to an engineer who joined the team last month. Follow the sequence:
- The wrong amount first appears on the refunds screen, which was built with VIPER. Does the fix belong in the Interactor that calculated the number, or in the Presenter that formatted it?
- The same wrong amount shows up on the account screen, which is MVVM. That screen has no Interactor at all. So does the fix go in its ViewModel?
- Both screens read from the data layer, and that layer is half migrated: some calls go through the new repository, some through the old
TransactionManager. Which of the two produced this number? - Before the new engineer can change a single line, they spend a day working out how this app is put together.
Three philosophies in one codebase means the question "where does this fix go?" has three answers, and every change begins with that investigation. A codebase that is uniformly average MVVM answers the question once. That is also why changing architecture is a migration, with a plan behind it — not something you do to a few files as you pass through them.
This lesson is the overview. The four lessons after it build the same ideas at production depth, on one app: Ledger, an app for moving money.
- D1-04 — MVVM in production. The screen-level pattern in depth: screen state modelled as one enum, who owns a view model and for how long, and navigation as a value you can test.
- D1-05 — Clean Architecture in production. Every layer of the send-money feature: entities, use cases, and the ports they own. It also covers turning the server's JSON into domain types, translating errors at each border, and splitting the layers into separate packages so that breaking the dependency rule becomes a compile error.
- D1-06 — a reference architecture. The whole app assembled on one page. This is the lesson to reread the night before a system-design interview.
- D1-07 — Tape. A second capstone: a real-time trading app, where the same vocabulary meets the opposite failure modes and some of Ledger's best patterns become bugs.
Your team of four is building a new SwiftUI app. It has subscription pricing with different rules per region, entitlements that must work offline, and three targets — app, widget and watch — sharing that logic. What do the three steps above give you?
A junior teammate asks: "The job advert 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 which two ideas are the same in every architecture, and which parts are only vocabulary.
You can now:
- Draw MVVM, Clean and VIPER, and name what talks to what
- Implement an entity, a use case and a repository with the dependency rule intact
- Explain why SwiftUI already does the jobs of VIPER's Presenter and Router
- Choose a dose of architecture for a given team — and defend consistency over purity
Next up: MVVM at production depth — the screen-level pattern, done properly on a real app.