← Writing
Architecture

MVVM vs VIPER, and when to ignore both

Every few months someone asks me which iOS architecture they should use. The honest answer takes longer than a conference talk allows, which is why the conference-talk answer — “MVVM with Combine” or “Clean Architecture” — gets repeated until it feels like received wisdom.

After eleven years shipping production iOS apps across five companies, in team sizes from 1 to 12 engineers, here is what I actually use and why.

What each pattern actually solves

MVC, MVVM, VIPER, and Clean Architecture each exist to solve a specific problem that appears at a specific scale. Choosing one without understanding the problem it solves leads to the most common iOS architecture mistake: adopting a pattern's ceremony without getting its benefit.

MVC solves: nothing, in the sense that it's the platform default. Apple's frameworks are MVC-shaped. UIKit delegates back to view controllers. A solo developer on a small app with UIKit will write maintainable MVC if they're disciplined about where business logic lives. The problem is that “disciplined” is hard to enforce in a team.

MVVM solves: testability of business logic without UI. The ViewModel is a plain class with no UIKit imports. It takes inputs, transforms them, and exposes outputs. SwiftUI makes MVVM natural — @StateObject and @ObservableObject are essentially first-class MVVM support. The failure mode is ViewModel bloat: when the ViewModel becomes the new Massive View Controller.

VIPER solves: team coordination on complex flows. Strict module boundaries (View, Interactor, Presenter, Entity, Router) mean two engineers can work on the same feature without merge conflicts. The cost is boilerplate — a simple list screen needs five files. The benefit is that the boundaries force you to write the contract first and the implementation second.

Clean Architecture solves: long-lived codebases that need to replace infrastructure. If you might switch from Core Data to a cloud database, or from URLSession to GraphQL, Clean's dependency inversion keeps that change contained. For most apps, this flexibility is hypothetical. For apps with 5+ years of runway, it pays off.

When I choose MVVM

MVVM is my default for SwiftUI apps with a team of one to three engineers. The pattern fits SwiftUI's data flow naturally, ViewModels are easy to unit test, and the overhead is low enough that you can move fast without sacrificing structure.

A good MVVM ViewModel for a list screen looks like this:

@MainActor
final class AppListViewModel: ObservableObject {
    @Published private(set) var apps: [App] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?

    private let repository: AppRepository

    init(repository: AppRepository = AppRepositoryImpl()) {
        self.repository = repository
    }

    func loadApps() async {
        isLoading = true
        defer { isLoading = false }
        do {
            apps = try await repository.fetchAll()
        } catch {
            self.error = error
        }
    }
}

Notice that AppRepository is a protocol. That single decision gives you testability — inject a mock in tests, inject the real implementation in production. Without it you have MVC with extra steps.

Where MVVM breaks down: when a single ViewModel starts coordinating between multiple screens. Navigation is the classic failure point. A ViewModel shouldn't know about other ViewModels. Once it does, you have implicit coupling that VIPER's Router layer would have made explicit.

When I choose VIPER

VIPER earns its boilerplate when the team has four or more iOS engineers and the features are complex enough that parallel development causes merge conflicts. I used VIPER at Shivam Jewels for the diamond cataloguing flow — 12 screens, 3 engineers working simultaneously, 3-month timeline.

The contract-first approach forced decisions early:

// Write the protocols first, before any implementation
protocol AppListPresenterProtocol: AnyObject {
    func viewDidLoad()
    func didSelectApp(_ app: App)
}

protocol AppListViewProtocol: AnyObject {
    func showApps(_ apps: [App])
    func showLoading(_ isLoading: Bool)
    func showError(_ message: String)
}

protocol AppListInteractorProtocol: AnyObject {
    func fetchApps() async throws -> [App]
}

protocol AppListRouterProtocol: AnyObject {
    func navigateToDetail(app: App)
}

Two engineers can implement the Interactor and the View simultaneously as soon as the protocols exist. Code review becomes easier — you're reviewing whether an implementation conforms to its contract, not guessing at intended behaviour.

The real cost of VIPER is not the boilerplate — code generation tools handle that. The real cost is that it takes engineers two to three weeks to internalise the boundaries well enough to make good decisions without a review. Budget for that ramp.

When I ignore both

For prototypes, tools, and internal utilities, I write whatever gets the job done. A two-screen admin panel does not need VIPER. A single-view SwiftUI widget does not need a ViewModel. Adding architectural ceremony to a 200-line file is waste.

The rule: architecture pays off when the cost of reading unfamiliar code exceeds the cost of writing boilerplate. For a file a solo engineer wrote last week, that crossover point is nowhere near.

The decision I actually make

Here is the decision tree I use at the start of a new iOS project:

The pattern is a tool. Misfitting a tool to a problem — using VIPER for a two-screen app, or using flat MVC for a six-engineer feature — costs more than the architectural decision saves. Match the pattern to the team size and timeline, not to what was in last year's conference talk.

One more thing: whichever pattern you choose, document the rationale in a short ADR (Architecture Decision Record) and enforce it in code review from day one. A well-applied MVC codebase beats an inconsistently applied VIPER codebase every time.