In several job postings on LinkedIn I kept seeing references to The Composable Architecture or TCA, asking applicants to at least be familiar with it. I wasn’t, so it seemed like a good thing to try and get to know a little bit.
The Setup
On GitHub I have a repo for what I call my SwiftUI Learning Space - an app with small independent screens where I can try out new ideas, implement concepts in isolation, and when new iOS versions come along add new screens to test out the new changes. It’s small right now with more TODOs than done things(it’s only about a week old) but that seemed like a perfect place to try out TCA.
The Work
I followed the TCA tutorial located here . It includes some basic concepts of Reducer
, Store
and State
information then builds up towards navigation. I haven’t finished the full navigation yet but I am well on my way towards it.
So Just What Is TCA?
It’s an architecture intended to build composable components in isolation to reduce code pollution, ease testing work, and most importantly be “composable” - you can add more than one reducer and run them in order without them having a direct connection with each other. It keeps your code cleanly-isolated and makes it possible to scale it quicker.
The Good
Probably what I was most impressed with is how easy it was to create a test store and have a test implementation of your reducer. Not just that but you could even pass a dedicated preview value as well. That clean separation makes it possible to know with a single look what test code is using and change it as needed.
Sticking with the testing theme, test failures and errors were actually readable by a human being! That was something mentioned in the tutorial linked above but when I ran into issues not mentioned in the tutorial it was great to have that information for debugging. For more information see Tutorial Troubles below.
Easy to read: With a glance at your reducer’s
State
andAction
it is straightforward to tell what a given reducer works on and what it can support.
The Bad
There is a ton of scaffolding. The tutorial builds up toward an app with a simple navigation that can add and delete
Contact
objects from a list. And that’s ~200 lines of code. That seems like alot for a basic use case and I think that speaks to a key point. This architecture is more useful for apps that are complex in order to keep them under control and make testing simple. It is too much scaffolding for simple apps.Reinvention of the wheel(Or is it?). There is a modifier called
ifLet
. My first thought was that this simply builds complexity over the app, but then I tried to follow through the code to see how it is defined and the underlying code is rather complex, so maybe this is just a way to simplify it.A ton of dependencies. I pulled this in using SPM and it pulled 12 different libraries(13 if you count an extra
XCTest
one). Again overkill for smaller projects.
Next Steps
In the GitHub repo I mentioned I intended implement TCA for navigation. Then in a branch I will replace TCA with The Clean Architecture and see which one should stay. Whichever does not stay will end up in a separate screen to keep and play with in isolation.
Tutorial Troubles
I don’t know how current that tutorial is, but I ran into two problems that cost me multiple hours(and questions on a Slack workspace) to fix.
Tests need to run on the main thread. This one was an example of how the error messages were very informative:
Thankfully the solution here was straightforward - just annotate your test case with
@MainActor
and the problem is fixed. So this one took five minutes to fix.This one was infuriating. I followed the tutorial’s instructions. I double and triple checked the code. I googled around. Yet the problem persisted. This was a case of the human-readable error message still not helping at all find the problem.
The answer: The tutorial tells you to implement a
TestStore
with a call towithDependencies
in order to override the dependency with a test implementation. But for me that never worked. The code got called, but it seems like what it created got eaten somewhere in the bowls of the system.
Eventually I asked in an iOS developer Slack workspace I am in and was introduces to theTestDependencyKey
protocol. You can adopt that in an extension and provide both apreviewValue
andtestValue
. Once you do that the system will call those and the problem goes away:
extension NumberFactClient : TestDependencyKey {
static let previewValue: NumberFactClient = Self(
fetch: { number in
return "\(number) is a good number."
})
static let testValue: NumberFactClient = Self(
fetch: { number in
return "\(number) is a good number."
})
}
Have a good weekend.