Getting Started With The Composable Architecture
Learn how to structure your iOS app with understandable and predictable state changes using Point-Free’s The Composable Architecture (TCA) framework. By David Piper.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With The Composable Architecture
30 mins
- Getting Started
- Exploring the Composable Architecture
- Understanding the Components of TCA
- Using the Composable Architecture
- Modeling States and Actions
- Accessing the Store From Views
- Sending Actions to the Store
- Handling Side Effects
- Managing Dependencies With an Environment
- Transforming State With Reducers
- Composing Features
- Sharing Dependencies With SystemEnvironment
- Combining States and Actions
- Adding Views to the App
- Composing Reducers
- Testing Reducers
- Creating a TestStore
- Testing Reducers Without Effects
- Testing Reducers With Effects
- Understanding Failing Tests
- Where to Go From Here?
Composing Reducers
The final step is to add repositoryReducer
to rootReducer
. Switch back to RootFeature.swift.
But how can a reducer working on local state, actions and environment work on the larger, global state, actions and environment? TCA provides two methods to do so:
- combine: Creates a new reducer by combining many reducers. It executes each given reducer in the order they are listed.
-
pullback: Transforms a given reducer so it can work on global state, actions and environment. It uses three methods, which you need to pass to
pullback
.
combine
is already used to create rootReducer
. Thus, you can add repositoryReducer
right after userReducer
‘s closing parenthesis, separating them with a comma:
// 1
repositoryReducer.pullback(
// 2
state: \.repositoryState,
// 3
action: /RootAction.repositoryAction,
// 4
environment: { _ in
.live(
environment: RepositoryEnvironment(repositoryRequest: repositoryEffect))
})
Here you see how to use pullback
. Although it’s just a few lines, there’s a lot going on. Here’s a detailed look at what’s happening:
-
pullback
transformsrepositoryReducer
to work onRootState
,RootAction
andRootEnvironment
. -
repositoryReducer
works on the localRepositoryState
. You use a a key path to plug out the local state from the globalRootState
. - A case path makes the local
RepositoryAction
accessible from the globalRootAction
. Case paths come with TCA and are like key paths, but work on enumeration cases. You can learn more about them at Point-Free: Case Paths. - Finally, you create an environment
repositoryReducer
can use. You useSystemEnvironment.live(environment:)
to start with the live environment defined in SystemEnvironment.swift. It already providesmainQueue
anddecoder
. Additionally, you create a new instance ofRepositoryEnvironment
usingrepositoryEffect
. Then, you embed it in the live environment.
Build and run the app. The repository feature and the user feature are working together to form one app:
Testing Reducers
One goal of TCA is testability. The main components to test are reducers. Because they transform the current state to a new state given an action, that’s what you’ll write tests for.
You’ll write two types of tests. First, you’ll test reducers with an action that produces no further effect. These tests run the reducer with the given action and compare the resulting state with an expected outcome.
The second type verifies a reducer which produces an effect. These tests use a test scheduler to check the expected outcome of the effect.
Creating a TestStore
Open RepoReporterTests.swift, which already includes an effect providing a dummy repository.
The first step is to create TestStore
. In testFavoriteButtonTapped
, add the following code:
let store = TestStore(
// 1
initialState: RepositoryState(),
reducer: repositoryReducer,
// 2
environment: SystemEnvironment(
environment: RepositoryEnvironment(repositoryRequest: testRepositoryEffect),
mainQueue: { self.testScheduler.eraseToAnyScheduler() },
decoder: { JSONDecoder() }))
This is what’s happening:
- You pass in the state and reducer you want to test.
- You create a new environment containing the test effect and test scheduler.
Testing Reducers Without Effects
Once the store is set up, you can verify that the reducer handles the action favoriteButtonTapped
. To do so, add the following code under the declaration of store
:
guard let testRepo = testRepositories.first else {
fatalError("Error in test setup")
}
store.send(.favoriteButtonTapped(testRepo)) { state in
state.favoriteRepositories.append(testRepo)
}
This sends the action favoriteButtonTapped
to the test store containing a dummy repository. When calling send
on the test store, you need to define a closure. Inside the closure, you define a new state that needs to match the state after the test store runs the reducer.
You expect repositoryReducer
to add testRepo
to favoriteRepositories
. Thus, you need to provide a store containing this repository in the list of favorite repositories.
The test store will execute repositoryReducer
. Then, it’ll compare the resulting state with the state after executing the closure. If they’re the same, the test passes, otherwise, it fails.
Run the test suite by pressing Command-U.
You’ll see a green check indicating the test passed.
Testing Reducers With Effects
Next, test the action onAppear
. With the live environment, this action produces a new effect to download repositories. To change this, use the same test store as above and add it to testOnAppear
:
let store = TestStore(
initialState: RepositoryState(),
reducer: repositoryReducer,
environment: SystemEnvironment(
environment: RepositoryEnvironment(repositoryRequest: testRepositoryEffect),
mainQueue: { self.testScheduler.eraseToAnyScheduler() },
decoder: { JSONDecoder() }))
This uses the test effect instead of an API call, providing a dummy repository.
Below the declaration of store
, add the following code:
store.send(.onAppear)
testScheduler.advance()
store.receive(.dataLoaded(.success(testRepositories))) { state in
state.repositories = self.testRepositories
}
Here you send onAppear
to the test store. This produces a new effect and testScheduler
needs to process it. That’s why you call advance
on the scheduler, so that it can execute this effect.
Finally, receive
verifies the next action that’s sent to the store. In this case, the next action is dataLoaded
, triggered by the effect created by onAppear
.
Run the tests. Again, you’ll see a green check, proving the handling of onAppear
is correct.
Understanding Failing Tests
The Composable Architecture provides a useful tool to understand failing tests.
In testFavoriteButtonTapped
, at the closure of send
, remove:
state.favoriteRepositories.append(testRepo)
Rerun the test, and it fails, as expected.
Click the red failure indicator and inspect the error message:
Because you removed the expected state from send
, the resulting state doesn’t match the expected empty state. Instead, the actual result contains one repository.
This way, TCA helps you understand the difference between the expected and actual state at first glance.
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
The ideas behind the TCA framework are very close to finite-state machines (FSMs) or finite-state automata (FSA) and the state design pattern. Although the Composable Architecture comes as a ready-to-use framework and you can adapt it without needing to learn many of the underlying mechanisms, the following materials can help you understand and put all the concepts together:
- Here is a good general overview of finite-state machines.
- Design patterns based on states is one of 23 design patterns documented by the Gang of Four. You can find a brief introduction on the State pattern webpage.
- Read The State Design Pattern vs. State Machine to see some practical aspects and differences.
Although FSM/FSA is a mathematical computation model, every time you have a switch
statement or a lot of if
statements in your code, it’s a good opportunity to simplify your code by using it. You can use either ready-to-use frameworks, like TCA, or your own code, which isn’t really difficult if you understand crucial base principles.
You can learn more about the framework at the GitHub page. TCA also comes with detailed documentation containing explanations and examples for each type and method used.
Another great resource to learn more about TCA and functional programming in general is the Point-Free website. Its videos provide a detailed tour of how the framework was created and how it works. Other videos present case studies and example apps built with TCA.
The Composable Architecture shares many ideas with Redux, a JavaScript library mainly used in React. To learn more about Redux and how to implement it without an additional framework, check out Getting a Redux Vibe Into SwiftUI.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!