iosNudge

Compare MVC, MVVM, VIPER and TCA. Which would you choose?

MediumAsked oftenArchitecture 3 min read

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 a Reducer. 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

PartJob
ViewShows data, sends user events
InteractorBusiness logic and data fetching
PresenterTurns data into display values, talks to view and router
EntityPlain model types
RouterNavigation 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

MVCMVVMVIPERTCA
BoilerplateLowMediumHighMedium
TestabilityLowGoodGoodVery good
Learning curveLowLowMediumHigh
SwiftUI fitPoorGoodPoorGood
Best forSmall apps, prototypesMost appsLarge UIKit teamsTeams 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. @State is 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.