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 3 of 4 of this article. Click here to view the first page.

Transforming State With Reducers

Reducer is a struct containing a function with the signature:

(inout State, Action, Environment) -> Effect<Action, Never>

This means that a reducer has three parameters it operates on, representing:

  • State
  • Action
  • Environment

The state is an inout parameter because it’s modified by the reducer depending on the given action. The reducer uses the environment to access the dependencies it contains.

The return type means the reducer can produce an effect that’s processed next. When no further effect needs to be executed, the reducer returns Effect.none.

Open RepositoryFeature.swift, which already contains an empty reducer returning an empty effect. Replace it with the following code:

// 1
let repositoryReducer = Reducer<
  RepositoryState,
  RepositoryAction,
  RepositoryEnvironment> 
  { state, action, environment in
  switch action {
  // 2
  case .onAppear:
    return environment.repositoryRequest(environment.decoder())
      .receive(on: environment.mainQueue())
      .catchToEffect()
      .map(RepositoryAction.dataLoaded)
  // 3
  case .dataLoaded(let result):
    switch result {
    case .success(let repositories):
      state.repositories = repositories
    case .failure(let error):
      break
    }
    return .none
  // 4
  case .favoriteButtonTapped(let repository):
    if state.favoriteRepositories.contains(repository) {
      state.favoriteRepositories.removeAll { $0 == repository }
    } else {
      state.favoriteRepositories.append(repository)
    }
    return .none
  }
}

Here’s what’s happening:

  1. repositoryReducer works on RepositoryState, RepositoryAction and RepositoryEnvironment. You have access to these inside the closure.
  2. The function of a reducer depends on the given action. In the case of onAppear, RepoReporter performs an API request to load repositories. To do so, the reducer uses repositoryRequest from the environment, which produces a new effect. But a reducer needs to return an effect with the same action type it can operate on. Thus, you need to map the effect’s output to RepositoryAction.
  3. In the case of dataLoaded, the reducer extracts the received repositories and updates the state. Then the reducer returns .none, because no further effect needs to be processed.
  4. The last action to handle is favoriteButtonTapped, which toggles the favorite status of a repository. If the given repository wasn’t favorited, it’s added to the state’s list of favorite repositories, and vice-versa.

Now that everything is set up, it’s time to see the repository feature in action. Open RepositoryView.swift.

If not visible, enable SwiftUI’s preview by clicking Adjust Editor Options in the upper-right corner of Xcode and selecting Canvas. Click Live Preview. The preview will look like this:

Preview of the repository feature build with TCA.

The repository feature is now finished and functional on its own. The next task is to combine it with the user feature. Composing separate features in one app is a main strength of the Composable Architecture. ;]

Composing Features

It’s time to combine the existing user feature with the repository feature you’ve just completed.

Open RootFeature.swift located in the Root group. It contains the same structure as the repository feature, but this time for the whole app. Currently, it only uses the user feature. Your task is now to add the repository feature as well.

Sharing Dependencies With SystemEnvironment

repositoryReducer uses DispatchQueue and JSONDecoder provided by RepositoryEnvironment. userReducer, which is declared in UserFeature.swift, also uses DispatchQueue and JSONDecoder. However, you don’t want to copy them and manage the same dependencies multiple times. You’ll explore a mechanism to share the same dependencies between separated features: SystemEnvironment.

Open SystemEnvironment.swift from the Shared group. It contains a struct called SystemEnvironment that holds all shared dependencies. In this case, it has DispatchQueue and JSONDecoder. It may also wrap a sub-environment like RepositoryEnvironment that contains feature-specific dependencies. In addition to this, two static methods, live(environment:) and dev(environment:), create pre-configured SystemEnvironment instances to use in a live app or while developing.

Now that you’ve learned about SystemEnvironment, it’s time to use it. Go to RepositoryFeature.swift and remove mainQueue, decoder and dev from RepositoryEnvironment. This leaves only repositoryRequest, which is specific to the repository feature.

Next, replace:

let repositoryReducer = Reducer<
  RepositoryState,
  RepositoryAction,
  RepositoryEnvironment>

With:

let repositoryReducer = Reducer<
  RepositoryState,
  RepositoryAction,
  SystemEnvironment<RepositoryEnvironment>>

This lets repositoryReducer use the shared dependencies from SystemEnvironment.

You just need to make one last change. Switch to RepositoryView.swift. Replace RepositoryListView in RepositoryListView_Previews with the following code:

RepositoryListView(
  store: Store(
    initialState: RepositoryState(),
    reducer: repositoryReducer,
    environment: .dev(
      environment: RepositoryEnvironment(
        repositoryRequest: dummyRepositoryEffect))))

Previously, you used RepositoryEnvironment when creating the store. repositoryReducer now works with SystemEnvironment. Thus, you need to use it when initializing Store instead.

Create a new SystemEnvironment using dev(environment:) and pass in RepositoryEnvironment using dummyRepositoryEffect instead of the live effect.

Build the project, and it will now compile without errors again. You won’t see any visible changes, but you’re now using the system environment’s dependencies in your repository feature.

Combining States and Actions

Next, it’s time to add all of the repository feature’s state and actions to the root state. The root feature is the main tab bar of your app. You’ll define the state and actions for this feature by combining the two features inside the app to create a single root feature.

Go back to RootFeature.swift. RootState represents the state of the whole app by having a property of each feature state. Add the repository state right below userState:

var repositoryState = RepositoryState()

Next, you’ll add RepositoryAction to RootAction. Similar to RootState, RootAction combines the actions of all separate features into one set of actions for the whole app. To include the repository feature actions, add them right below userAction(UserAction):

case repositoryAction(RepositoryAction)

In your app, the user can do two things on the root view: See repositories and see a user profile, so you define an action for each of those features.

Adding Views to the App

Open RootView.swift. RootView is the main view of RepoReporter.

Two tabs with Color.clear act as placeholders for the repository feature views. Replace the first Color.clear with:

RepositoryListView(
  store: store.scope(
    state: \.repositoryState,
    action: RootAction.repositoryAction))

This code initializes a new RepositoryListView and passes in a store. scope transforms the global store to a local store, so RepositoryListView can focus on its local state and actions. It has no access to the global state or actions.

Next, replace the second Color.clear with:

FavoritesListView(
  store: store.scope(
    state: \.repositoryState,
    action: RootAction.repositoryAction))

This time, add FavoritesListView as a tab. Again, scope transforms global state and actions to local state and actions.