App architecture · advanced

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.

14 min read14 min practice0/2 exercises5 recall cards

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
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

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.

A full Clean slice: entity rule → use case → repository seam → two infrastructures.

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:

RoleOwnsIn the refund example
Viewpixels and user events, passiveshows the result label
Interactorthe business operationRefundOrder — same idea as the use case
Presentermediates: formats for the view, routes intentsturns the outcome into label text
Entitythe data typesOrder
Routernavigation, screen assemblybuilds 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

MVVMCleanVIPER
Roles per screen23–55 + protocols
Domain isolated from frameworksif you're disciplinedby constructionyes
Ceremony per featurelowmediumhigh
SwiftUI fitnaturalgood (Clean core + MVVM shell)poor
Sweet spotmost apps, small teamscomplex domain, multiple frontendshuge UIKit teams, uniformity above all

The honest decision procedure:

  1. Start with MVVM-shaped separation — it is the SwiftUI grain, and every later architecture contains it.
  2. 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.
  3. 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.

Check yourself

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

Write the code

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.

Solution unlocks after 3 attempts

Same feature, three vocabularies

Write the code

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.

Solution unlocks after 3 attempts
Teach itSaved on this device. Never graded.

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.

Checkpoint

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.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.