← All Writing

Exploring Modern iOS Architectures

MVC, MVVM, MVP and VIPER — the same TMDb movie app built four ways, compared on orthogonality, ease of use, and testability.

This project structures my knowledge of modern architectural patterns by building the same movie app four ways — MVC, MVVM, MVP, and VIPER. The app uses the TMDb API (with static JSON files included for quick runs), so the only thing that changes between targets is the architecture.

The TMDb movie app — results, detail, and cast screens

The same app across every target: a results grid, a movie detail, and a cast list.

The code is written with readability and learning in mind — using these in production may call for performance work or other changes. As I work through each one I keep three questions in view.

MVC

MVC — Model, View, and Controller

Apple’s MVC: the Controller mediates between Model and View.

  • Model
    • Conceptual domain model, representing real-state content
    • Data-layer objects and business logic (stores, managers)
  • View
    • Cells, UIView objects and subclasses
    • Sends actions to the Controller
  • Controller
    • Mediator between View and Model
    • Delegate and data source of almost everything
    • Dispatches and cancels network requests
    • Manages the View lifecycle (tightly coupled)

The Controller ends up owning almost everything — which is why MVC is the quickest to stand up and the hardest to keep orthogonal as a screen grows.

MVVM

MVVM — Model, View, ViewModel, and a binder

MVVM: the ViewModel is a UIKit-free abstraction of the View, connected through bindings.

  • Model — conceptual domain model, representing real-state content
  • View — structure, layout, and appearance (UIViewControllers, cells, UIViews); updates its state from the ViewModel via bindings and forwards events to it
  • ViewModel — an abstraction of the View exposing public properties and commands; a UIKit-independent representation of the View
  • Binder — a (reactive) data-binding technology is key to the pattern; this simple example achieves binding through callbacks instead

The defining rule: the ViewModel never imports UIKit. It exposes display-ready values and a closure the View binds to, so the View stays thin and the formatting logic becomes testable in isolation.

MovieViewModel.swift
// MVVM ViewModels should not access UIKit
final class MovieViewModel: ViewModelInterface {
private var movies = [Movie]() { didSet { modelUpdate?() } }
var count: Int { return movies.count }
// The View binds a reload closure; the ViewModel invokes it on model change.
private var modelUpdate: (() -> Void)?
func bindViewReload(with modelUpdate: @escaping () -> Void) {
self.modelUpdate = modelUpdate
}
func fetchNewModelObjects() {
DataManager.shared.fetchNewTmdbObjects(withType: .movie) { result in
switch result {
case let .success(transportables):
self.movies = transportables as! [Movie]
case let .failure(error):
print(error)
}
}
}
// viewModel[i] -> a UIKit-free, display-ready value
subscript (index: Int) -> Transportable { return presentableInstance(from: movies[index]) }
}

MVP

MVP — Model, View, and Presenter

MVP: a passive View delegates everything to the Presenter.

  • Model — conceptual domain model, representing real-state content
  • View — passive UIViewControllers, cells, UIViews; may not know about the Model or execute presentations; delegates user interactions to the Presenter
  • Presenter — acts upon the Model and the View; holds the logic for user interactions; retrieves data from the Model and formats it for display

MVP draws the View/Presenter boundary with protocols. The View only knows how to reload and show; the Presenter owns everything else — which makes the Presenter trivially testable against a mock View.

MVP — the passive View + the Presenter contract
// The View is passive: it only knows how to reload and show.
protocol ResultsView: class {
func reloadCollectionData()
func show(_ detailVC: DetailViewController)
}
// The Presenter holds the logic and formats data for the View.
protocol ResultsViewPresenter: CollectionViewConfigurable {
var objectsCount: Int { get }
init(view: ResultsView)
func presentableInstance(index: Int) -> Transportable
func presentNewObjects()
func presentDetail(for indexPath: IndexPath)
}

VIPER

VIPER — View, Interactor, Presenter, Entity, Router

VIPER: business logic moves into the Interactor; navigation into the Router.

  • View — displays what the Presenter tells it to and relays user input back
  • Interactor — contains business logic for a use case; its work is independent of any UI
  • Presenter — prepares content for display (received from the Interactor) and reacts to user input
  • Entity — conceptual domain model, only manipulated by the Interactor
  • Routing — navigation logic; shared between the Presenter and a Wireframe that handles transition animations

VIPER takes the MVP idea to its conclusion: every collaborator is a protocol boundary. The repo has separate protocol files for the View, Presenter, Interactor, and Router — maximum separation and testability, at the cost of many small files.

VIPER — everything is a protocol boundary
protocol ResultsPresenterInterface: class {
func updateView()
func showDetail(forPresentable object: Transportable)
func collectionConfiguration() -> CollectionViewConfigurable
}

References

The full Xcode project — all four architectures behind one app — lives in the project repository on GitHub.