← Async Digital

English Cymraeg

Working notes ·

One line, fourteen packages

Async Digital Ltd Cardiff, UK

Abstract

I added The Composable Architecture to an empty Swift package and built it. One dependency line resolved to 14 packages and compiled 45 Swift modules. About 40,000 lines of code end up linked into the app. Another 140,000 compile on a clean command-line build so the macros can run, and 96% of those are Apple's Swift parser. That price bought real things when SwiftUI's state tools were weak and people typed every line themselves. Apple shipped Observation in 2023, and an agent writes most of my boilerplate now. So this note asks what a third-party architecture still buys, and when I would still pay for it. My answer leans on three things: staying close to what Apple ships, keeping less code, and getting reliable work from agents through rules you own.

I am not against Point-Free. Last year I argued in public for their dependencies library over SwiftUI's @Environment. But a library you can remove in an afternoon is one decision. A framework that sets the shape of every feature in the app is a different one, and that is the decision this note is about.

§1·Measure

What one line brings in

I did not want to quote someone else's numbers, so I made them. The test is a Swift package with nothing in it except a dependency on TCA 1.26.2, the current release, and one small feature written with @Reducer and @ObservableState. Then a clean build.

WhatCount
Packages you declare1
Packages resolved and downloaded14
Of those, from Point-Free12
Swift modules compiled45
Modules linked into the app21
Modules that only run the macros at build time24

Lines of code, counting only files the compiler actually compiled. Blank lines and comments are left out.

Where the code goesCode lines
Linked into the app40,294
of which TCA itself12,042
Compiled to run macros, never shipped139,619
of which swift-syntax134,165
Total compiled on a clean build179,913

Two things stand out. Only 30% of the code compiled into the app is TCA. The other 70% is the packages it stands on: navigation, collections, sharing, custom dump, case paths, clocks, and more. And the biggest single cost never reaches a user. It is swift-syntax, compiled so that @Reducer can expand.

These are SwiftPM command-line builds. I did not measure Xcode. SwiftPM can use a prebuilt swift-syntax and has that switched on by default. For 603.0.2, the version TCA resolved, no prebuilt was downloaded, so it compiled from source. When I pinned 602.0.0, SwiftPM did download a prebuilt, and swift-syntax still compiled from source. I have not worked out why. Read the 140,000 as the command-line cost without prebuilts. Your Xcode build may differ.

§2·Case

What people were paying for

If I skip the case for TCA, this note is a strawman, so here it is.

TestStore makes a test assert every state change an action causes. Forget one and the test fails. That is a stronger guarantee than most test suites ever get. Every dependency is swapped per test through one mechanism. Features compose into parents in a single, checked way. And a team of 20 gets one answer to "where does this side effect go", which is worth a lot when 20 people are asking.

It also fixed real pain. Before iOS 17, ObservableObject redrew views that had not read the property that changed, and driving navigation from state was hard to get right. TCA had answers to both while Apple did not.

§3·Apple

Apple closed most of the gaps

Apple has never named an architecture, so I will not say it recommends one. What it showed in Discover Observation in SwiftUI at WWDC23 is a plain class marked @Observable that a view reads directly. A view redraws only when a property it actually read changes. People call that pattern model-view, or MV.

That fixed the redraw problem at the language level. Models pass down through @Environment. Navigation stacks bind to state. Swift Testing arrived in 2024 with parameterised tests and #expect. All of it ships with the OS or the toolchain, so it adds nothing to the package graph and nothing to a clean build.

What Apple did not ship is anything like TestStore. Nothing in the platform forces a test to account for every state change. If that guarantee matters to you, it is still TCA's strongest card.

§4·AI

What AI changed, and what it did not

Part of any framework's appeal is less code to write. A reducer, its actions, and its test follow a pattern, and a pattern is typing. An agent does that typing for me now, in either architecture, in seconds.

Writing was rarely the expensive part, though. Reading, reviewing, and debugging are. In my work AI has made those cheaper too, but far less than it made writing. They still need a person who understands the code, and they still grow with how much code there is to understand. A framework does not remove those costs either. It moves them. I read my feature plus the framework's rules. I debug by stepping through a generic reducer I did not write. And the framework adds a cost of its own: I upgrade when the vendor ships, on the vendor's schedule.

So the question has changed. It used to be whether a framework saved more typing than it cost. Typing is close to free now, so the question is whether it saves more reading, reviewing, and debugging than it adds. That is a harder test to pass with 40,000 lines in the app.

§5·Rails

The best argument for TCA in 2026

The strongest argument for TCA in 2026 is not that it makes code faster to write. An agent has largely removed that advantage.

It is that TCA gives an AI agent a constrained way to work.

An agent can put a network call in a SwiftUI view and the compiler will happily accept it. TCA does not change that. What it does provide is a strong convention for where effects belong, a reducer model that makes those effects explicit, a dependency system that fails a test when a feature reaches a dependency with no test implementation, and TestStore, the strongest card from §3, which can fail when a state change is left unasserted.

That matters because an agent is very good at producing code that compiles without necessarily preserving the architecture you intended.

But those rails do not have to come from a framework.

I agree that agents need constraints. I disagree that the constraints need to come from TCA. The same principle can be applied at the repository level: write the conventions down, then enforce the rules that matter with ordinary tooling.

The distinction is important. A rule in an agent prompt is advice. A conventions file is better, but it is still advice unless something checks it. The useful pattern is to put the rule in prose and then put an independent check behind it.

For example, suppose the rule is that views do not perform networking. The conventions file explains that rule and why it exists. A lint rule can then reject networking APIs in view files, both during development and in CI. The agent can review its own changes against the conventions as well, but that is advisory. The lint rule is the enforcement because it does not depend on the model agreeing with it. It holds only while switching it off takes a person, so a disable comment, or a change to the lint configuration, needs a human to approve it.

This is the approach I describe in Mastering the AI Coworker, which I sell. Its chapter on hooks closes its takeaway on one line: "A rule is real when breaking it takes deliberate effort from a human, not restraint from a model."

TCA provides some of these constraints as part of the architecture. Building them yourself means maintaining your own rules and checks. That is real work. But it also means the constraints describe your application rather than the framework's model of one.

There is one part I would not try to reproduce: exhaustive state assertion. TestStore is genuinely useful here. Nothing in SwiftUI or Swift Testing gives you the same guarantee that a test will account for every state change caused by an action.

There is also a subtle cost to vendor-provided rails: they move. TCA 1.26.2 still contains WithViewStore, deprecated in favour of the Observation-based approach introduced in 1.7. Public Swift code contains both patterns, so an agent trained on it has almost certainly seen both. Only one is current.

I have not measured how often that produces stale code, so I will not claim that it does. But it illustrates the trade-off. When the architecture comes from a framework, the framework's migrations become part of your application's migrations.

And TCA 1.26.2 already includes an opt-in setting whose description says to enable it "to remain ready for Composable Architecture 2.0".

The question, then, is not whether TCA provides rails. It does.

The question is whether those rails are worth adopting an architecture, its dependencies and its migration path for the whole application.

§6·Costs

The costs that do not shrink

None of these get cheaper because an agent is writing the code.

§7·Platform

Stay close to the platform, and keep less code

If this note had room for one argument, it would be this one. Stay as close to what Apple ships as you can, and keep as little code as you can.

Every line of third-party code in your app is a line you did not write, and you still own it when it breaks.

Look at §1 again with that in mind. Observation, @Environment, and Swift Testing come from Apple, with the OS or the toolchain. TCA compiles 40,294 lines into the app before you write a feature. Of those, 5,668 are swift-collections, which is Apple's own package. The other 34,626 are third-party code inside your app, upgraded on a third party's schedule. On the platform path that number starts at zero. I cannot claim a model-view app always has less code of its own, because I have not measured that.

Staying close to the platform does not mean avoiding change. Apple introduced @Observable in 2023 as the new way to drive SwiftUI, and apps that wanted its benefits had to migrate. A framework does not shield you from that. It adds its own migration on top. TCA had to absorb Observation too, and one of its 14 packages, swift-perception, exists to back-port Apple's observation tools to older OS versions. On the platform path there is one migration, documented by the company that ships the OS, arriving with an SDK you have to adopt anyway.

And less code is what makes everything in §4 cheaper. Typing is close to free. Reading, reviewing, and debugging are not, and all three grow with the code you own or have to understand. The platform default keeps that number lowest.

§8·Still

When I would still pick it

There are cases where I would choose TCA today.

§9·Mine

What I do instead

My apps use @Observable models read directly by views. The rules live in a written conventions file that agents read before they start. Two checks run outside the model, both in the agent harness rather than the repository, and both only on edits made through the agent's file-editing tools. SwiftLint fixes what it can in each Swift file edited that way. A hook stops to ask me before such an edit adds a swiftlint:disable comment. Neither sits in the repository yet, which is where §5 puts the lint rule that runs in CI, so both are weaker than what I describe there. A review agent reads changes against the conventions file too, but that is a model, so I count it as advice. The networking rule in §5 is an example of a check you could add, not one my apps carry today.

For dependency injection it depends on the app. Some use @Environment with Observation, which fixed the redraw part of what I complained about in 2025. Some still use Point-Free's dependencies library. That is a library-sized choice, but it is not free either. It pulls in swift-syntax too, so on those apps the platform path does not start at zero.

I should say that I have a stake here. My business builds apps this way. The book quoted in §5 is mine, and it is for sale. I also publish a package other apps can adopt, Iris, which routes incoming URLs into typed SwiftUI navigation. It shapes more than a small utility does, because an app declares one coordinator for its navigation. But it stops at navigation. An app adopts it for routing and deep links, and its features stay plain SwiftUI. Point-Free sells subscriptions that teach TCA, so they have one too. None of those changes the numbers in §1, which anyone can reproduce with an empty package and a clean build.

If you are deciding this for a new app, I would ask four questions. How many people will change this code? Do you need every state change asserted, or is testing behaviour enough? How much vendor-driven upgrade work can you absorb in a year? And are your written conventions backed by checks that run outside the model, of the kind described in §5? If the answer to the last one is yes, you already have rails, and you may not need to download 14 packages to get them.

Method how this note was made

Measured on 13 September 2026 against TCA 1.26.2, released 28 August 2026. A package containing one reducer and nothing else, built clean with SwiftPM on Swift 6.2.4, macOS, arm64, debug.

A module counts as compiled only if it produced object files. SwiftPM creates build folders for 90 targets, and counting those would have doubled the figure. Lines come from each module's own compiler source list, so tests, examples, and documentation snippets are excluded. My line counter was checked against a hand-counted file before I trusted it.

This was a macOS build. An iOS build swaps the AppKit navigation module for the UIKit one, so a module count can move by one or two. Compiled is not the same as shipped. I did not measure how much survives dead-code stripping into a binary.

I worked on this with an AI agent, which ran the builds and counted. The question, and the choice to measure rather than quote, were mine.