Compare MVC, MVVM, VIPER and TCA. Which would you choose?
TL;DR
All four patterns try to answer one question: where does the logic live?
- MVC: in the view controller. Simple, but it grows into a "Massive View Controller".
- MVVM: in a view model that the view watches. The most common choice today, and natural in SwiftUI.
- VIPER: split into View, Interactor, Presenter, Entity, Router. Very strict, lots of files.
- TCA: one
State,Actions and aReducer. Very testable, but you add a library and learn new ideas.
A good answer: "It depends on team size and app size. For most apps I use MVVM with a router for navigation."
MVC (Apple's classic)
final class ProfileViewController: UIViewController {
var user: User?
override func viewDidLoad() {
super.viewDidLoad()
Task {
user = try await API.shared.fetchUser() // networking
nameLabel.text = user?.name.capitalized // formatting
}
}
}The controller does networking, formatting, navigation and view setup. It is hard to test because you need UIKit to test logic.
MVVM
@MainActor @Observable
final class ProfileViewModel {
private(set) var title = ""
private(set) var isLoading = false
private let api: UserAPI
init(api: UserAPI) { self.api = api }
func load() async {
isLoading = true
defer { isLoading = false }
let user = try? await api.fetchUser()
title = user?.name.capitalized ?? "Unknown"
}
}
struct ProfileView: View {
@State var model: ProfileViewModel
var body: some View {
Text(model.title).task { await model.load() }
}
}The view model knows nothing about UIKit or SwiftUI views. You can test it with a fake UserAPI.
Navigation is often moved out into a coordinator or router.
VIPER
| Part | Job |
|---|---|
| View | Shows data, sends user events |
| Interactor | Business logic and data fetching |
| Presenter | Turns data into display values, talks to view and router |
| Entity | Plain model types |
| Router | Navigation and module building |
protocol ProfilePresenting { func viewDidLoad() }
protocol ProfileInteracting { func fetchUser() async throws -> User }
protocol ProfileRouting { func showSettings() }Good separation and testing, but five or more files per screen. Often too heavy for small teams.
TCA (The Composable Architecture)
A library from Point-Free. Ideas from Redux and Elm.
@Reducer
struct Counter {
@ObservableState
struct State: Equatable { var count = 0 }
enum Action { case incrementTapped }
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .incrementTapped:
state.count += 1
return .none
}
}
}
}All changes go through actions. Side effects return as Effects. Its TestStore checks every state change exactly.
Downsides: learning curve, a third-party dependency, and some extra boilerplate.
Comparison
| MVC | MVVM | VIPER | TCA | |
|---|---|---|---|---|
| Boilerplate | Low | Medium | High | Medium |
| Testability | Low | Good | Good | Very good |
| Learning curve | Low | Low | Medium | High |
| SwiftUI fit | Poor | Good | Poor | Good |
| Best for | Small apps, prototypes | Most apps | Large UIKit teams | Teams that want strict, predictable state |
What interviewers want to hear
- You know the trade-offs, not only the names.
- Logic should be testable without UI.
- Dependencies are injected, not global.
- Navigation has a clear owner.
- You would not force VIPER on a two-person team.
Common follow-up questions
- Is MVVM needed in SwiftUI? Not for tiny views.
@Stateis enough. Use a view model when there is real logic or async work. - Where does navigation go in MVVM? In a coordinator (UIKit) or a router with a navigation path (SwiftUI).
- What is "Massive View Controller"? A controller that does everything. Fix it by extracting view models, services and child controllers.
- What is Clean Architecture? Layers (UI, domain, data) where inner layers do not depend on outer ones. It often sits under MVVM.