Exploring The Composable Architecture - Part 1
Exploring The Composable Architecture (TCA) - Part 1
The Composable Architecture (TCA, for short) is a library for building applications consistently and understandably, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
Think of each screen as a small app, that can be run, tested, and debugged on its own, while at the end being able to compose it with other screens to form a full complex app.
TCA building blocks
I noticed many similarities with TCA and Redux for React, the idea is each screen should have the following building blocks which can be later composed in larger units that form a complex app.
A type that holds the current state of the application. usually represented as a struct.
A type that holds all possible actions that cause the state of the application to change. usually represented as an enum which —in combination with a switch— breaks in build time if one of the cases is not handled.
A type that encapsulates a unit of work that can be run in the outside world, and can feed data back to the Store. such as network requests, saving/loading from disk, creating timers, etc.
A type that holds all dependencies needed to produce Effects, such as API clients, analytics clients, random number generators, etc.
Think of the reducer as the brain that describes how to evolve the current state of an application to the next state, given some action, and describes what effects should be executed later by the store —if any—.
A store represents the runtime that powers the application. It is the object that you will pass around to views that need to interact with the application.
Back to our todos app, the app is a one-screen app, but could be divided into two main pieces:
- Todo: the logic for interacting with a single todo item
- Todos: the logic for interacting with a list of todo items.
The state for a single todo item is a simple struct that represents what a todo is.
The action is an enum representing all actions that can be performed on a single todo item.
The environment is a place that stores all external dependencies required to create a todo, for now, it's going to be an empty struct.
The reducer is the brain that contains all business logic for manipulating the state, it takes an
inoutstate, an action to perform, and an environment to get external dependencies from.
The reducer can then emit effects when an action is performed, in this case, both actions do not emit any effects.
The view needs a store to get state from and perform actions on. notice how the UI is wrapped in a
WithViewStorewrapper, this ensures the view is only updated when there is a state change and provides a
viewStoreproperty that can be used to get state and send actions to the store to perform.
Unlike plain SwiftUI with Combine, all state changes should be handled by the store, so to provide a binding string to the text field, TCA comes with a
bindingmethod that defines a getter and setter for a given value.
The model for the todos screen is a struct that has an
Todoitems, this helps the view to loop over all items and connect them to a list, more on this later.
Like before, the action is an enum representing all actions that can be performed in the todos screen. Notice how the last one takes an id for a todo and a single todo action to perform on it!
This is an amazing feature of TCA, the environment provides two things:
- a closure that is responsible for creating a UUID, we will create different closures for production and test, more on this later.
- a scheduler that we'll use to delay sorting the checked todos, think of this as a
DispatchQueuethat we will be able to swap in tests with a test scheduler, the environment enables us to control time!
The reducer is a combination of two reducers here:
The single todo reducer pulled back to work on each item on the todos list. Notice the
\TodosAction.todo, it's a case path, think of it as a key path but to enums.
a new reducer that handles all other actions in the screen, however, we're also handling the
.todo(.checkBoxToggled)case here to emit an effect to wait 1 second after the last checkbox is toggled and sort the todos all at once for a better user experience.
While we could have used a simple string for the debounced id here, using a private
Hashabletype ensures that the effect can not be mixed with other debounced effects throughout the app.
TodoView, the view needs a store to get state from and perform actions on.
Notice how the list uses a
ForEachStorethat scopes the store to a sub-store for each todo item providing it with a single todo state, and pulling back its action to the
Putting everything together
Stating the app becomes as easy as creating a store and passing it to the
One nice thing we have here is replacing the entry point for the app with the single todo screen can be done effortlessly by changing the store with a single todo store, and passing it to a
TodoView instead, this also enables us to create slices of the app and distribute it easily in more complex apps.
We saw before how we combined two reducers in the todos screen, TCA promotes combining many reducers into a single one by running each one on the state in order and merging all of the effects.
This enables endless use cases like the
debug reducer that comes with TCA, which prints debug messages describing all received actions and state mutations, simple by chaining it to any reducer!
Chaining it with the
todoReducer will print the following debug messages in the console
You can create a reducer that logs data, cache responses, send analytics events! Let's create a reducer that caches the state after each update, so when we restart the app we get it right back where it was before quitting!
Building a reducer to persist app state
To start we will make both
TodosState conform to
Next, let's define a
Caching protocol that represents an object that can save
Codable objects with a given key.
We can then create a
UserDefaultsCache which saves the state to
DocumentsCache that saves the state to the documents directory
Or another one that saves it to a CoreData or realm database ...
See how the reducer is generic enough that can be used on any
We can extend the
Reducer where its
State conforms to
Codable, and provide a
caching(cache:) function that takes any
Caching store and uses it to save the state every time it changes.
fireAndForget, we're asking the reducer to fire an effect in the background, where we don't care about getting the result back to the store.
Even nicer we can provide an optional
isDuplicate closure that compares the state before and after performing an action and save the state only when it changes!
Imagine we have many more properties in the
TodosStateand all we care about is the todos list, this will ignore caching state changes that do not affect the todos list.
We're almost done, all we need to do is to load the state from the cache and pass it to the store's initial state, and make sure the same cache is chained to the store's reducer!
Great testability is one of TCA's great features! Since all state mutations are happening in reducers, it is easy to test almost everything in the app with simple unit tests!
Those Environment objects provide a quick way to swap production dependencies with test ones.
To start, let's create a deterministic, auto-incrementing "UUID" generator for testing.
Here is an example of how to test adding a new todo item
TCA comes with a
TestStore class that has a closure in its
send method that can be used to verify state changes.
TCA looks very promising, I enjoyed working with it 😊 and can't wait to use it in more complex apps to see how it plays with more advanced topics like navigation and UIKit.
The code above is a simpler version of the full Todos app with some bells and whistles and more tests available on Github.
That’s it for now. If you have any questions, suggestions, or feedback, please let me know via Twitter 👋