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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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:

  1. pullback transforms repositoryReducer to work on RootState, RootAction and RootEnvironment.
  2. repositoryReducer works on the local RepositoryState. You use a a key path to plug out the local state from the global RootState.
  3. A case path makes the local RepositoryAction accessible from the global RootAction. 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.
  4. Finally, you create an environment repositoryReducer can use. You use SystemEnvironment.live(environment:) to start with the live environment defined in SystemEnvironment.swift. It already provides mainQueue and decoder. Additionally, you create a new instance of RepositoryEnvironment using repositoryEffect. 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:

Final state of RepoReporter. The repository feature and the user feature are combined to one single 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:

  1. You pass in the state and reducer you want to test.
  2. 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.

The Composable Architecture app with tests showing as passing in Xcode

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:

An error message of a failed test with TCA shows a detailed explanation of expected and actual state.

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:

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!