design · folio
In one line: Every iOS architecture answers three questions: who owns the state,
which way does data flow, and where do side effects live. VIP (Clean
Swift) makes each screen a one-way cycle of three objects; RIBs (Uber) drive the app
by a tree of business logic, not of views; MVI and Redux keep one
immutable state changed only by a reducer; TCA is Redux made composable; MV says
SwiftUI views plus @Observable models are already enough.
Download PDF Print view LaTeX source
| Pattern | State lives in | Data flow | Testability | Boilerplate | Fits |
|---|---|---|---|---|---|
| VIP | Interactor (+ DataStore) | VC→I→P→VC, one way | high: spy the next role | high: 7 per scene | UIKit apps, per-scene rigour |
| RIBs | each RIB’s Interactor | tree; Rx down, listener up | high: I/R/B behind protocols | very high, codegen | huge apps, hundreds of engineers |
| MVI | one immutable screen State | Intent→Model→View | high: pure reducer | medium | complex screens, Android parity |
| Redux / ReSwift | one global Store | dispatch→reducer→subscribers | high reducers; effects harder | medium–high | shared app-wide state |
| TCA | Store tree of features | unidirectional, composed | very high: TestStore | high | big SwiftUI teams |
| MV | @Observable models | view calls model, observes it | models yes, view logic no | low | SwiftUI small–mid apps |
How it works
- VIP (Clean Swift; Clean Architecture per scene): VC (
DisplayLogic) sends a Request to the Interactor (BusinessLogic, uses Workers, holds theDataStore); its Response goes to the Presenter (PresentationLogic), which formats a ViewModel and callsdisplay…on the VC. Router (RoutingLogic+DataPassing) navigates, passing data via theDataStore. Models nest per use case (ListOrders.FetchOrders.Request). Older templates:…Input/…Outputprotocols, wired by a Configurator. - RIBs = Router · Interactor · Builder (+ Component for dependencies, optional Presenter and View). The Interactor holds business logic and Rx subscriptions, alive between
didBecomeActive()andwillResignActive(); bind withdisposeOnDeactivate(interactor:). Router attaches/detaches children; Builder creates the unit and is the only part that knows the DI system. Up: aListenerthe parent implements; down: Rx streams orbuild()arguments. - MVI: Intent turns user events into actions, Model turns actions into state, View renders state — a pure
render(state)of one immutable state. - Redux’s 3 principles: one store = single source of truth · state is read-only, changed only by an action · pure reducers (state + action → new state). ReSwift:
Store<AppState>(reducer:state:),dispatch,newState(state:);subscribe(self) { $0.select(…) }narrows updates, skipping repeats of anEquatablesubstate. - MV: no per-screen ViewModel; logic lives in
@Observablemodels (iOS 17, macOS 14). A view updates only when a property itsbodyreads changes. Own with@State, share with.environment(model), read with@Environment(Model.self), bind with@Bindable. - TCA: State · Action · Reducer · Store; effects and dependencies are injectable, and
TestStoreasserts every state change and effect.
Why RIBs exist
Uber’s rider app (rewrite, 2016): an MVC app grown to a few hundred engineers, one RequestViewController from 300 to 3,000+ lines. RIBs make business logic, not
the view tree, drive routing — a deep logic tree with a shallow view tree.
Example — one VIP use case
enum ListOrders { enum Fetch { // per use case
struct Request {}
struct Response { let orders: [Order] }
struct ViewModel { let rows: [String] } } }
protocol ListOrdersDisplayLogic: AnyObject {
func display(_ vm: ListOrders.Fetch.ViewModel) }
final class ListOrdersInteractor { // BusinessLogic
var presenter: ListOrdersPresenter?; let worker = OrdersWorker()
func fetch(_ r: ListOrders.Fetch.Request) {
worker.load { self.presenter?.present(.init(orders: $0)) } } }
final class ListOrdersPresenter { // PresentationLogic
weak var viewController: ListOrdersDisplayLogic? // no cycle
func present(_ r: ListOrders.Fetch.Response) {
viewController?.display(.init(rows: r.orders.map(\.title))) } }
Traps(unverified)
- “VIP is VIPER renamed.” VIPER’s Presenter sits between View and Interactor, two-way; VIP is a one-way cycle, no Entity layer, typed models per use case.
- RIBs’ Router is not “just navigation” — it owns the RIB tree; screens are a side effect of attaching RIBs.
- Redux: every subscriber hears every change unless it
selects a slice; keep ephemeral UI state (focus, scroll) out of the global store. - MVI: a toast stored in State fires again on re-render — model it as a consumed event.
- “MV means no tests” — the logic moves to
@Observablemodels, which test fine; what you lose is a per-screen seam.