A blog exploring functional programming and Swift.

Announcing SwitchStore for the Composable Architecture

Monday Jun 14, 2021

This week’s episode took a deep dive into how we can embrace some of Swift’s most important data modeling tools (optionals and enums) in the Composable Architecture without sacrificing composition of our application’s behavior. We showed that one our very own case study applications, the Tic-Tac-Toe app demo, modeled its root state in a less than ideal fashion: using two optional values instead of an enum. This allows invalid states to be representable in our application, which leaks into application logic making it more complex, and so we’d love to make those states unrepresentable by the compiler.

Unfortunately, the Composable Architecture does not come with the tools necessary to properly use enums for state… well, until today that is! We are releasing a new version of the library that adds a pullback method on Reducer and a SwitchStore view that are specifically tuned for breaking down behaviors modeled on enum state into behavior for each case of the enum.

Reducer.pullback

We often want to model the state of a part of our applications using an enum to represent mutually exclusive states. For example, at the root of our application we may separate the logged-in and logged-out states into cases of an enum:

enum AppState {
  case loggedIn(LoggedInState)
  case loggedOut(LoggedOutState)
}

In the Composable Architecture we like to define reducers on sub-state and then use the pullback and combine operators to piece multiple reducers together into one big reducer that operates on bigger pieces of state. The pullback operator accomplishes this by using a WritableKeyPath to extract out sub-state, operate on it, and then plug it back into the whole state. So we would hope we could define a reducer for each of the logged-in domain and logged-out domain that could then be pieced together to operate on the entire app domain.

However, when state is modeled as an enum we do not have access to key paths. We instead need a way to try to extract a particular case from the state enum, operate on it, and then embed it back into the state enum. This is precisely what case paths excel at, which is the analogous concept for key paths, but tuned specifically for enums instead.

So, rather than pulling back a reducer along a key path to some sub-state we can instead pull back along a case path to a sub-case:

let loggedInReducer: Reducer<LoggedInState, LoggedInAction, LoggedInEnvironment> = ...

let loggedOutReducer: Reducer<LoggedOutState, LoggedOutAction, LoggedOutEnvironment> = ...

let appReducer = Reducer.combine(
  loggedInReducer.pullback(
    state: /AppState.loggedIn,
    action: /AppAction.loggedIn,
    environment: { LoggedInEnvironment(...) }
  ),

  loggedOutReducer.pullback(
    state: /AppState.loggedOut,
    action: /AppAction.loggedOut,
    environment: { LoggedOutEnvironment(...) }
  )
)

SwitchStore

While the pullback operator helps us compose the logic of our application, the SwitchStore view helps us compose the behavior of our application. It serves the same purpose that the IfLetStore serves for optionals and the ForEachStore serves for collections, but is tuned specifically for enums. It allows you to destructure a store into multiple stores, one for each case of your state’s enum.

For example, if we had a LoggedInView and LoggedOutView to represent the root view for each of the logged in and out states, then we could “switch” on the root store in order to figure out which view to display:

SwitchStore(self.store) {
  CaseLet(state: /AppState.loggedIn, action: AppAction.loggedIn) { loggedInStore in
    LoggedInView(store: loggedInStore)
  }

  CaseLet(state: /AppState.loggedOut, action: AppAction.loggedOut) { loggedOutStore in
    LoggedOutView(store: loggedOutStore)
  }
}

Under the hood the SwitchStore view figures out whenever your state enum’s case changes from the .loggedIn case to the .loggedOut case (or vice-versa), and will make sure the correct view is displayed. You can also leverage SwiftUI’s transition APIs to automatically animate when the views appear or disappear:

SwitchStore(self.store) {
  CaseLet(state: /AppState.loggedIn, action: AppAction.loggedIn) { loggedInStore in
    LoggedInView(store: loggedInStore)
      .transition(.opacity.combined(with: .offset(x: 0, y: 20))
  }

  CaseLet(state: /AppState.loggedOut, action: AppAction.loggedOut) { loggedOutStore in
    LoggedOutView(store: loggedOutStore)
      .transition(.opacity)
  }
}

This will make it so that the LoggedInView appears with a crossfade and a small vertical translation, whereas the LoggedOutView will just appear with a crossfade.

Performance

The SwitchStore view has a few tricks up its sleeve in order to do its job with the best performance possible. If implemented naively, the SwitchStore would re-compute its body when any piece of state changes inside the enum. However, we only care when the state enum changes from one case to another case so that we can show the respective view. We don’t need to know about all the changes within a particular case.

By taking advantage of Swift’s powerful metadata embedded in every Swift program we can write a function that determines the case of any enum, and so so blazingly fast. This metadata is the same info that SwiftUI uses to its seemingly magic behavior. It’s an advanced feature of Swift that doesn’t get enough attention, but it allows the SwitchStore to minimize the number of times it needs to re-compute its body.

Try it today

Upgrade to the latest version of swift-composable-architecture to immediately gain access to these tools today, and enhance your applications with the expressiveness of enum state. We’ve already made use of SwitchStore to refactor the Tic-Tac-Toe demo to improve its domain modeling by using proper enums instead of two optionals 🥳.


Subscribe to Point-Free

👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series on functional programming and Swift.