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:
- Solo engineer, SwiftUI, short timeline? Plain MVVM with protocol-based repositories. Move fast, test the ViewModel, don't over-engineer.
- Two to three engineers, SwiftUI or UIKit mixed? MVVM with a shared Coordinator for navigation. Add a domain layer (use cases) if the business logic is genuinely complex.
- Four or more engineers, complex feature flows, parallel development? VIPER. Use a code generator (Generamba or a custom Xcode template) to reduce boilerplate. Write the protocols in a kick-off session before anyone writes implementation.
- Five-plus year app with likely infrastructure changes? Clean Architecture with dependency injection. Worth the investment.
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.