Skip to main content
There is no single best iOS architecture — there is a best fit per app size, team size and risk profile, and the honest way to choose is an axis-by-axis comparison. This page scores the six mainstream choices — MVC, MVVM, Clean Architecture, VIPER, RIBs and TCA (the Composable Architecture) — plus Duet, Modaal’s cross-platform parity framework for native iOS and Android apps, on the axes the practitioner literature actually argues about, and adds the two axes that literature does not score: whether the shape survives a second platform, and how well an AI coding agent can work inside it. Last reviewed: September 1, 2026. Library versions and repository states were checked against each project’s official repository on this date.

How do the architectures compare?

Two disclosures about this table. The gradations are this page’s own synthesis: the criteria come from the practitioner sources cited throughout — a Medium survey of the patterns (per-app-type fit guidance and per-pattern characterizations), the tailec/ios-architecture sample collection, and the r/iOSProgramming thread on scalable architecture — all commentary, none of which publishes a scored matrix like this one. And the Duet column is written by Duet’s vendor; the criteria definitions and the concessions below are how this page keeps that column honest. The criteria rows encode what the r/iOSProgramming thread’s practitioners argue about. Its most-quoted definition, from user danielt1263: “A good architecture does two things. 1. Separates logic from effects (user interactions, network calls and DB reads/writes) so that the logic is easily testable. 2. Separates features from each other (as much as possible) so that it’s easy to add/remove/update a feature without breaking other features.” The table’s testability and separation rows score exactly those two properties.

TCA: the closest relative to Duet

The Composable Architecture (Point-Free; 1.26.2 as of August 2026, ~15k stars) is the strongest of the six at making state, actions and effects explicit — and it is the architecture Duet learned from. The mapping, concept by concept:
The two departures have one reason: a recording must be able to assert effects. An opaque Effect closure can be run but not compared, so Duet’s reducers return effect descriptions — data with equality — and the fixture asserts exactly what was requested, in what order. The same requirement moves dependencies from @Dependency’s resolver into named environment protocols: what the logic needs is part of its recorded contract, and each platform supplies its implementation as a worker. What TCA does better. Its composition operators are a genuinely powerful way to build big features from small ones, and Duet has no equivalent — composition happens structurally, in shells. Its dependency system (swift-dependencies) is more ergonomic than hand-named environment protocols. Its TestStore is mature, exhaustive by default, with a documented non-exhaustive mode for integration tests. And its observation is finer-grained: since TCA 1.7, views observe the individual state fields they read, so a change re-renders only the views touching those fields — Duet’s shells re-render per whole-state emission and lean on the platform’s diffing below that. What TCA adds to the build. In Modaal’s build-lane measurement on a toy feature package, adding TCA brought 14 dependency packages into the build and moved a cold swift test from 7 seconds to 55 seconds. That is a build-and-CI cost, not a runtime-performance claim, and a toy-scale number — but the dependency surface is also what one thread commenter meant by disliking that TCA “relies heavily on a third party library”: the architecture arrives as a substantial external dependency, where MVC and MVVM arrive as conventions and Duet arrives as scaffolded project structure plus a small runtime of its own. Choose TCA over Duet for a Swift-only product that wants maximal in-language power — composition operators, the dependency system, per-field observation — and does not need a second platform verified against the same behavior. TCA is Swift; Duet’s reducer compiles to both platforms and its recordings gate both apps.

RIBs and VIPER: the module-boundary lineage

VIPER (Mutual Mobile, published on objc.io in 2014) applied Clean Architecture’s separation to iOS screens: View, Interactor, Presenter, Entity, Router, each behind a protocol. RIBs is Uber’s descendant — Router, Interactor, Builder, with optional Presenter and View — built for apps where the business tree, not the screen stack, drives structure, and proven at Uber scale as two parallel implementations, one per platform (the rider-app engineering post tells that story). The iOS library now lives in its own actively maintained repository, uber/RIBs-iOS (1.1.0, July 2026), still built on an RxSwift backbone; the original repo continues as Android-only. The concession leads here, because it is the lineage’s whole argument: these architectures’ protocol seams are best-in-class. Every boundary is a protocol; everything is mockable; a team of thirty can work one app without treading on each other. RIBs adds a real innovation Duet inherits in spirit — the business-logic tree owns navigation, and screens are leaves, not owners. The cost is per-screen ceremony. In Modaal’s conversion work, a literal RIBs leaf shell — the files a screen needs before any feature logic exists — measured about 101 lines across 6 declarations, against a 28-line hand-rolled equivalent for the same screen; that overhead recurs on every screen the app adds. Duet’s position is that most of what the seams buy — mockability, isolation, navigation owned by logic — is bought instead by the pure-reducer-plus-effects shape and the recorded contract, without the per-screen protocol suite; VIPER’s presenter layer, specifically, is replaced by state projection. No internal measurements exist for VIPER itself — the cited sources above carry its scoring. Choose RIBs when a large team needs enforced module ownership on a business-tree-shaped app and accepts the ceremony; its seams remain the reference point. Teams searching for a “RIBs successor” for a two-platform product are the readers Duet’s row in the table addresses: the same tree-owns-navigation instinct, with the parallel-implementation problem replaced by one verified core.

MVC and MVVM: the starting points

MVC is Apple’s original template shape: fastest first screen, no structural contract. Its documented failure mode — the Medium survey calls it out as the pattern’s major drawback — is the massive view controller: logic accretes in the controller because nothing says where else it goes. For a prototype or a 1–2 person app that will stay small, that trade is fine, and the practitioner thread’s top-voted advice points the same way. MVVM is the reactive upgrade SwiftUI made near-default: views bind to an observable ViewModel, and — as the survey puts it — the ViewModel tests well because it holds no UI. With @Observable, SwiftUI is MVVM-shaped out of the box. Two limits keep it mid-table. Nothing enforces unidirectionality or where effects live: each ViewModel author decides, so testability degrades screen by screen as shortcuts accrete (one thread commenter: moving logic into the view “makes it completely untestable”). And nothing verifies a port — an Android twin of an MVVM app shares an acronym, not behavior. The thread’s most-voted comment is still the right small-app default: “MVVM is a pretty good default to use until you have a compelling reason to want something fancier.” This page agrees, and the table’s rows are the compelling reasons enumerated. For the Android-side view of the same judgment, see how Duet compares to a hand-rolled ViewModel.

Clean Architecture: layers held by convention

Clean Architecture (Robert C. Martin, 2012) gives the strongest separation on paper: entities, use-cases, interface adapters, frameworks, with dependencies pointing inward. On iOS it produces highly testable use-cases and the highest ceremony of the six, and its enforcement mechanism is the one to be clear-eyed about: layer boundaries live in convention and code review — no build gate fails when a use-case imports a framework type. Teams that keep Clean clean do it with discipline and lint rules they write themselves. Duet’s mapping: the use-case layer corresponds to the reducer plus its environment; the Dependency Rule corresponds to the scaffolded rule that reducers take no platform imports. What Duet adds is an executable form of the contract — recordings that replay in CI — and what Clean keeps that Duet gives up is freedom: any per-layer idiom, any binding style, any test strategy. That freedom is why Clean fits large single-platform teams with strong review culture, and why the thread’s enterprise-scale defenders argue for it in exactly those terms.

Is RxSwift or Combine an architecture?

No — reactive programming is a binding substrate that several of the architectures sit on, and treating it as a competing architecture (the survey covers “Reactive Programming” as a seventh pattern) conflates two independent decisions. The evidence is the tailec/ios-architecture collection — a 2019–2022 sample archive, UIKit/RxSwift vintage, and still the clearest demonstration that the same MVVM ships with four different binding styles (closures, subjects, observables, functions). Pick the architecture first; the binding style is a separate, smaller decision. The substrate landscape today: RxSwift remains actively released (6.10.2, March 2026, with 2–3 releases a year tracking new Xcode and Swift versions) and still underpins RIBs-iOS; Combine ships with no deprecation flags in Apple’s documentation, while community commentary places Apple’s investment in Swift Concurrency and Observation. Duet’s stance on the substrate: streams live in adapters at the edges; feature logic consumes plain values and returns effect data. A subscription bag inside logic is the one shape that blocks conversion to a shared core — which is why the Combine-to-coroutines operator mapping matters at the edges and nowhere else.

Which architecture should you choose?

  • A prototype or an app that stays small: MVC or plain SwiftUI-MVVM. The ceremony rows of the table are costs you would pay for nothing.
  • A growing single-platform app with a few engineers: MVVM, upgraded with discipline where it hurts — or TCA when deterministic state, exhaustive tests and in-language composition are worth its concept load and dependency surface.
  • A large single-platform team needing enforced ownership: RIBs (business-tree apps, maximal seams) or Clean (strong review culture, maximal freedom inside layers). VIPER today is mostly the maintained-codebase case; new adoption has largely moved to its descendants.
  • A product where Android exists or is coming: this is the axis that reorders the table. MVC, MVVM, Clean and VIPER offer a second platform nothing but conventions; RIBs offers a proven pattern implemented twice; TCA is Swift. Duet was built for this row — one Kotlin core compiled into both apps, with recorded fixtures as the cross-platform behavior gate — and whether to share at all is its own decision, upstream of this page.
  • A codebase an AI agent will build with you: weight the AI-agent row. An agent is exactly the collaborator that benefits from mechanical gates between steps — the argument, with the scaffold’s step order, is made in why this suits AI coding agents.

Common questions

The thread’s onboarding reports are real — one commenter: “I am STRUGGLING to understand TCA at my new job. All the concepts and functions are so foreign to me.” The concept load pays for exhaustive testability and composition; on a small app with simple state, that is paying for capacity you will not use, which is what the table’s over-engineering row scores.
On iOS, MVP’s niche was absorbed: its presenter idea lives on inside VIPER and RIBs, and MVVM took the plain-screen case once bindings became first-class. The sample archives still carry MVP implementations, but current iOS literature and hiring treat it as historical rather than a live option.
SwiftUI answers the binding question — views observe state natively — which is why MVVM’s fit score is high and VIPER’s is poor. It does not answer where effects live, who owns navigation, what a test can prove, or what happens on a second platform. Those four questions are the table’s remaining rows, and they are architecture questions regardless of toolkit.
The unidirectional reducer shape exists across the Kotlin world as MVI (and in libraries like MVIKotlin and Decompose), but TCA itself is a Swift library. Duet is one answer to the search behind this question: the TCA-derived reducer shape written once in Kotlin, compiled into both apps, and verified with recordings rather than ported by hand.
Yes, and large apps usually do — a RIBs tree with MVVM leaves, or a Clean core behind MVC screens. The cost is per-boundary translation and a harder onboarding story. The one mix that reliably fails is architecture-per-author in the same layer, which is the MVVM drift mode the table’s separation row already prices in.
Modaal scaffolds Duet projects with this page’s trade-offs already decided — modaal.dev.

Sources and further reading

Authorities — each architecture’s own source: Commentary — shaped this page’s axes, labeled as such:

How to build testable Android apps

The same reducer-and-recordings argument on the Android side, with code.

The best cross-platform frameworks in 2026

One level up: the framework survey this page’s cross-platform row points into.

Duet overview

The product form of the table’s last row — what the scaffold fixes and what it asks.

Migrating a CombineRIBs app

For teams on a RIBs-family codebase today: the per-feature route to a shared core.
Last modified on September 3, 2026