How do the architectures compare?
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: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
Is TCA too complex for a small app?
Is TCA too complex for a small app?
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.
Why isn't MVP (Model-View-Presenter) in the table?
Why isn't MVP (Model-View-Presenter) in the table?
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.
Does SwiftUI make architecture choice irrelevant?
Does SwiftUI make architecture choice irrelevant?
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.
Is there a Composable Architecture for Android or Kotlin?
Is there a Composable Architecture for Android or Kotlin?
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.
Can I mix architectures in one app?
Can I mix architectures in one app?
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:- swift-composable-architecture — Point-Free’s TCA repository and documentation
- uber/RIBs and uber/RIBs-iOS — the two RIBs repositories; Engineering the architecture behind Uber’s new rider app — the introducing post
- Architecting iOS apps with VIPER — the origin article, objc.io issue 13
- The Clean Architecture — Robert C. Martin’s post; the book expands it
- Apple’s Observation documentation — the substrate under current MVVM and TCA observation
- RxSwift — the reactive substrate under RIBs-iOS and pre-Combine MVVM
- Modern iOS architecture patterns and best practices — per-app-type fit guidance across six patterns
- What is the best architecture to build scalable and robust apps? — the practitioner criteria this page’s table rows encode
- tailec/ios-architecture — working samples of MVC, MVP, four MVVM binding variants and VIPER (a 2019–2022 archive)
Read next
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.