> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modaal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Best iOS App Architecture: TCA, RIBs, VIPER, MVVM, Clean

> TCA, RIBs, VIPER, MVVM, MVC and Clean Architecture scored on testability, team size, onboarding, cross-platform reach and AI-agent fit — with cited sources.

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**](/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?

<div className="sticky-col">
  |                                        | MVC                                  | MVVM                                           | Clean Architecture                                 | VIPER                            | RIBs                                           | TCA                                  | Duet                                                |
  | -------------------------------------- | ------------------------------------ | ---------------------------------------------- | -------------------------------------------------- | -------------------------------- | ---------------------------------------------- | ------------------------------------ | --------------------------------------------------- |
  | **Complexity & boilerplate**           | Minimal                              | Low                                            | High                                               | High                             | High                                           | Medium–high                          | Medium                                              |
  | **Development speed**                  | Fastest first screen                 | Fast                                           | Slower                                             | Slow                             | Slow                                           | Medium                               | Medium                                              |
  | **Testability**                        | Low — logic accretes in controllers  | Medium — ViewModels test well, flow unenforced | High — use-cases isolate logic                     | High — per-module protocols      | High — best-in-class seams                     | High — exhaustive TestStore          | High — recorded scenarios replay in CI              |
  | **Separation of concerns**             | Weak in practice                     | Per-screen                                     | Strong on paper, held by review                    | Strong, with per-screen ceremony | Best-in-class                                  | Strong                               | Strong — the seams are scaffolded                   |
  | **Pays off at**                        | 1–2 devs                             | 2–6 devs                                       | 6+ devs                                            | 8+ devs                          | 10+ devs                                       | 4+ devs                              | Any size with an agent; 2+ devs by hand             |
  | **SwiftUI fit**                        | UIKit-era; SwiftUI dissolves the "C" | Natural — `@Observable` is the pattern         | Neutral                                            | Poor — UIKit vintage             | Neutral — UIKit-first history                  | Excellent — per-field observation    | Native SwiftUI shells                               |
  | **Onboarding**                         | Immediate                            | Fast                                           | Slow — layer map first                             | Slow                             | Slow                                           | Slow — real concept load             | Medium                                              |
  | **Over-engineering risk (small apps)** | None                                 | Low                                            | High                                               | High                             | High                                           | Medium                               | Medium                                              |
  | **Cross-platform reach**               | None                                 | Convention only — nothing verifies a port      | The layer map ports; every layer is re-implemented | None built in                    | Proven at Uber as two parallel implementations | Swift-only                           | Built in — one compiled core, both platforms, gated |
  | **AI-agent fit**                       | Weak — no mechanical gates           | Medium — testable units, seams unenforced      | Medium — rules live in review, not the build       | Medium — many files per screen   | Medium — strong seams, heavy ceremony          | Good — exhaustive tests gate changes | Built for it — recordings gate each step            |
</div>

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](https://medium.com/@sharmapraveen91/modern-ios-architecture-patterns-and-best-practices-5320e2d9d1aa) (per-app-type fit guidance and per-pattern characterizations), the [tailec/ios-architecture sample collection](https://github.com/tailec/ios-architecture), and the [r/iOSProgramming thread on scalable architecture](https://www.reddit.com/r/iOSProgramming/comments/194o9w5/) — 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](https://github.com/pointfreeco/swift-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:

<Frame>
  | TCA                                   | Duet                                                  |
  | ------------------------------------- | ----------------------------------------------------- |
  | State struct with `@ObservableState`  | Serializable feature state — same role                |
  | `Action` enum                         | Sealed action set — kept as-is                        |
  | Reducer body — pure state transitions | A pure reducer per feature — kept, with attribution   |
  | `Effect` — an opaque async closure    | Effect *values* — equatable data a worker performs    |
  | `@Dependency` injection               | Environment protocols behind the effect seam          |
  | Exhaustive `TestStore`                | A recorded scenario, replayed on both platforms in CI |
  | `scope` and the composition operators | Composition in shells and builders, not operators     |
  | Cancel-in-flight effect identity      | Kept — cancellation expressed as data                 |
</Frame>

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](/articles/duet-glossary#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](https://www.objc.io/issues/13-architecture/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](https://github.com/uber/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](https://www.uber.com/blog/new-rider-app-architecture/) tells that story). The iOS library now lives in its own actively maintained repository, [uber/RIBs-iOS](https://github.com/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](/articles/testable-android-apps#what-does-a-blank-project-leave-open-that-duet-fixes).

## Clean Architecture: layers held by convention

[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) (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](/articles/duet-glossary#fixture) 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](https://github.com/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](https://github.com/ReactiveX/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](/articles/duet#what-you-get) — and [whether to share at all](/articles/share-code-ios-android) 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](/articles/testable-android-apps#why-does-this-suit-ai-coding-agents).

## Common questions

<AccordionGroup>
  <Accordion title="Is TCA too complex for a small app?" icon="scale-unbalanced">
    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.
  </Accordion>

  <Accordion title="Why isn't MVP (Model-View-Presenter) in the table?" icon="table-list">
    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.
  </Accordion>

  <Accordion title="Does SwiftUI make architecture choice irrelevant?" icon="swift">
    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.
  </Accordion>

  <Accordion title="Is there a Composable Architecture for Android or Kotlin?" icon="android">
    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.
  </Accordion>

  <Accordion title="Can I mix architectures in one app?" icon="layer-group">
    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.
  </Accordion>
</AccordionGroup>

<Note>
  Modaal scaffolds Duet projects with this page's trade-offs already decided — [modaal.dev](https://modaal.dev).
</Note>

## Sources and further reading

**Authorities — each architecture's own source:**

* [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture) — Point-Free's TCA repository and documentation
* [uber/RIBs](https://github.com/uber/RIBs) and [uber/RIBs-iOS](https://github.com/uber/RIBs-iOS) — the two RIBs repositories; [Engineering the architecture behind Uber's new rider app](https://www.uber.com/blog/new-rider-app-architecture/) — the introducing post
* [Architecting iOS apps with VIPER](https://www.objc.io/issues/13-architecture/viper/) — the origin article, objc.io issue 13
* [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) — Robert C. Martin's post; the book expands it
* [Apple's Observation documentation](https://developer.apple.com/documentation/observation) — the substrate under current MVVM and TCA observation
* [RxSwift](https://github.com/ReactiveX/RxSwift) — the reactive substrate under RIBs-iOS and pre-Combine MVVM

**Commentary — shaped this page's axes, labeled as such:**

* [Modern iOS architecture patterns and best practices](https://medium.com/@sharmapraveen91/modern-ios-architecture-patterns-and-best-practices-5320e2d9d1aa) — per-app-type fit guidance across six patterns
* [What is the best architecture to build scalable and robust apps?](https://www.reddit.com/r/iOSProgramming/comments/194o9w5/) — the practitioner criteria this page's table rows encode
* [tailec/ios-architecture](https://github.com/tailec/ios-architecture) — working samples of MVC, MVP, four MVVM binding variants and VIPER (a 2019–2022 archive)

## Read next

<CardGroup cols={2}>
  <Card title="How to build testable Android apps" icon="vial-circle-check" href="/articles/testable-android-apps">
    The same reducer-and-recordings argument on the Android side, with code.
  </Card>

  <Card title="The best cross-platform frameworks in 2026" icon="map" href="/articles/best-cross-platform-framework">
    One level up: the framework survey this page's cross-platform row points into.
  </Card>

  <Card title="Duet overview" icon="mobile-screen-button" href="/articles/duet">
    The product form of the table's last row — what the scaffold fixes and what it asks.
  </Card>

  <Card title="Migrating a CombineRIBs app" icon="arrow-right-arrow-left" href="/articles/combineribs-to-duet">
    For teams on a RIBs-family codebase today: the per-feature route to a shared core.
  </Card>
</CardGroup>
