VIP · RIBs · MVI · Redux · MV — the architecture map

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

VIP · RIBs · MVI · Redux · MV — the architecture map — figure 1

PatternState lives inData flowTestabilityBoilerplateFits
VIPInteractor (+ DataStore)VC→I→P→VC, one wayhigh: spy the next rolehigh: 7 per sceneUIKit apps, per-scene rigour
RIBseach RIB’s Interactortree; Rx down, listener uphigh: I/R/B behind protocolsvery high, codegenhuge apps, hundreds of engineers
MVIone immutable screen StateIntent→Model→Viewhigh: pure reducermediumcomplex screens, Android parity
Redux / ReSwiftone global Storedispatch→reducer→subscribershigh reducers; effects hardermedium–highshared app-wide state
TCAStore tree of featuresunidirectional, composedvery high: TestStorehighbig SwiftUI teams
MV@Observable modelsview calls model, observes itmodels yes, view logic nolowSwiftUI small–mid apps

VIP · RIBs · MVI · Redux · MV — the architecture map — figure 2

How it works

  • VIP (Clean Swift; Clean Architecture per scene): VC (DisplayLogic) sends a Request to the Interactor (BusinessLogic, uses Workers, holds the DataStore); its Response goes to the Presenter (PresentationLogic), which formats a ViewModel and calls display… on the VC. Router (RoutingLogic + DataPassing) navigates, passing data via the DataStore. Models nest per use case (ListOrders.FetchOrders.Request). Older templates: …Input/ …Output protocols, 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() and willResignActive(); bind with disposeOnDeactivate(interactor:). Router attaches/detaches children; Builder creates the unit and is the only part that knows the DI system. Up: a Listener the parent implements; down: Rx streams or build() 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 an Equatable substate.
  • MV: no per-screen ViewModel; logic lives in @Observable models (iOS 17, macOS 14). A view updates only when a property its body reads 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 TestStore asserts 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 @Observable models, which test fine; what you lose is a per-screen seam.

Edit on GitHub ·Report a problem