Video #149: Derived Behavior: Optionals and Enums
Episode: Video #149 Date: Jun 14, 2021 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep149-derived-behavior-optionals-and-enums

Description
We will explore two more domain transformations in the Composable Architecture. One comes with the library: the ability to embed a smaller domain, optionally, in a larger domain. Another we will build from scratch: the ability to embed smaller domains in the cases of an enum!
Video
Cloudflare Stream video ID: d005c10af0abfeddcd31cd3d80065735 Local file: video_149_derived-behavior-optionals-and-enums.mp4 *(download with --video 149)*
References
- Discussions
- GitHub Discussion: CaseLetStore (for example)
- Luke Redpath
- Composable Architecture Release 0.19.0
- 0149-derived-behavior-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
And just like that we have a pretty comprehensive test that exercises multiple parts of the application and even exercises how multiple domains interact with each other. The fact that we can execute logic in the domain of a single counter amongst an entire list of counters is kind of amazing. Just imagine trying to get this kind of testability and modularity with a vanilla SwiftUI view model. It’s hard to picture, but we will actually be taking a look at that soon enough.
— 0:35
However, before that we want to explore a few more tools for transforming domains. We just saw that the Composable Architecture comes with a tool for transforming a reducer on a local domain into one that works on an entire collection of that domain, along with a tool for transforming a store of a collection of domain into a store that focuses on just one single element of the collection.
— 0:59
That’s cool, but there are a lot of other data structures we may want to pick apart in that way. For example, what about optional state? Or more generally enum state? It’s possible to build tools similar to the .forEach higher-order reducer and ForEachStore view except that they work on optionals and enums instead of collections. To explore these concepts we are going to add a feature to our little toy application.
— 1:23
What if we didn’t want to show a simple alert when we got the fact from the API but instead wanted to show a banner at the bottom of the screen. And to make things a little more complex, the banner will itself have behavior of its own. We’ll add a button in the banner that allows you to fetch another, and it will even manage a little loading indicator that is displayed while the API request is in flight.
— 1:45
Let’s dig in. Optional state: a fact prompt domain
— 1:47
Let’s start by going into the counter feature and commenting out the code that displays the alert when the fact is received from the API: case let .factResponse(.success(fact)): // state.alert = .init(message: fact, title: "Fact") return .none
— 2:07
We will still show the alert if the fact request fails.
— 2:11
Instead of this alert we want to show a banner at the bottom of the screen over the list of counters. Unfortunately this banner view cannot easily be localized to live inside the counter view. It has no choice but to be added to the root view so that it can be placed over the list. Let’s quickly sketch out a stub of what that view could look like.
— 2:43
We can start by wrapping the entire list view in a ZStack with bottom alignment: ZStack(alignment: .bottom) { List { … } … }
— 2:57
Now any views we add below the list will be added to the bottom of the screen and over the list.
— 3:03
Let’s paste in a view that will display a number fact in a prompt there: VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "info.circle.fill") Text("Fact") } .font(.title3.bold()) Text("42 is a good number.") } HStack(spacing: 12) { Button("Get another fact") { } Button("Dismiss") { } } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.white) .cornerRadius(8) .shadow(color: .black.opacity(0.1), radius: 20) .padding()
— 3:24
With that we have a decent looking banner at the bottom of the screen. However, the view is getting pretty big and making a mess of our AppView , so let’s extract it into it’s own view: struct FactPrompt: View { var body: some View { VStack(alignment: .leading, spacing: 16) { … } } }
— 3:52
Let’s now model the full domain and logic for this one little feature. It’s pretty simple, but it does involve effects so that always makes things a little more complicated.
— 4:03
The state of the feature should at least contain a string for the current fact that is being displayed: struct FactPromptState: Equatable { var fact: String }
— 4:19
We will also need the count of the number we are displaying a fact for because when we try fetching a new fact we’ll need that number: struct FactPromptState: Equatable { let count: Int var fact: String }
— 4:41
And then finally we will also hold onto an isLoading boolean, which we can use to show and hide a loading indicator while the API request is inflight: struct FactPromptState: Equatable { let count: Int var fact: String var isLoading = false }
— 4:52
Next we have the enum of actions for the feature. There’s an action for tapping each of the buttons, as well as an action for getting a response from the fact API client: enum FactPromptAction: Equatable { case dismissButtonTapped case getAnotherFactButtonTapped case factResponse(Result<String, FactClient.Error>) }
— 5:18
And the last piece of the domain is the environment of dependencies the feature needs to do its job. For this feature we just need the FactClient and a main queue scheduler: struct FactPromptEnvironment { var fact: FactClient let mainQueue: AnySchedulerOf<DispatchQueue> }
— 5:36
Now we’re ready to implement a reducer for the feature’s logic: let factPromptReducer = Reducer< FactPromptState, FactPromptAction, FactPromptEnvironment > { state, action, environment in switch action { case .dismissButtonTapped: return .none case .getAnotherFactButtonTapped: state.isLoading = true return environment.fact.fetch(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(FactPromptAction.factResponse) case let .factResponse(.success(fact)): state.isLoading = false state.fact = fact return .none case .factResponse(.failure): state.isLoading = false return .none } }
— 7:18
Next we can introduce a store to the FactPromptView to bring the view to life: struct FactPrompt: View { let store: Store<FactPromptState, FactPromptAction> … }
— 7:29
We can wrap the body of the view in a WithViewStore so that we can start observing its state changes and send it actions: WithViewStore(self.store) { viewStore in … }
— 7:38
And then we can start making use of the state in the view store. For example, we can now check if the fact is currently loading so that we know whether we should show the progress indicator or a text view of the fact, and we can start sending actions to the view store from the buttons: if viewStore.isLoading { ProgressView() } else { Text(viewStore.fact) } … Button("Get another fact") { viewStore.send(.getAnotherFactButtonTapped, animation: .default) }) { } Button("Dismiss") { viewStore.send(.dismissButtonTapped, animation: .default) }
— 8:14
That completes this little feature. We could even plug it into our previews to see that it runs in complete isolation. And even better, the domain, logic and view has been defined in complete isolation from all other parts of the application. This means we could throw this feature into its own module so that it compiles in isolation, not only making it a better experience to work on this feature in terms of compile times and compiler stability, but it also strengthens the boundary between this feature and the rest of the app, making for a much better code base.
— 8:38
We won’t go that far for this episode, but it is now time to plug this feature into the main app feature. We can start by introducing the fact prompt domain to the full app state domain. For example, the AppState struct can now hold some FactPromptState : struct AppState: Equatable { var counters: IdentifiedArrayOf<CounterRowState> var factPrompt: FactPromptState }
— 8:59
Now this isn’t entirely correct. It’s not true that we always have FactPromptState available to us. The prompt only appears when we get a fact response from the API, and so there are times that no prompt is shown at all.
— 9:12
One thing we could do to try to accommodate for this is introduce a boolean that determines if the prompt is visible or not: struct AppState: Equatable { var counters: IdentifiedArrayOf<CounterRowState> var factPrompt: FactPromptState var isFactPromptVisible = false }
— 9:23
But this isn’t a great way to model the problem. It is quite weird for this boolean to be false and yet still have access to the prompt’s state. That leaves us open to introducing strange logic bugs in which we start accessing and changing the state inside factPrompt thinking that the prompt is visible, but in reality it isn’t.
— 9:44
A far better way to represent the showing and hiding of the fact prompt is to model it with optional state: struct AppState: Equatable { var counters: IdentifiedArrayOf<CounterRowState> var factPrompt: FactPromptState? }
— 9:52
This makes it impossible to have access to FactPromptState when the prompt isn’t visible and greatly strengthens our application logic. This trick is also crucial for understanding navigation in SwiftUI, which is a big topic we will be tackling very soon.
— 10:08
Next we can add the FactPromptAction s to the AppAction enum: enum AppAction: Equatable { case addButtonTapped case counterRow(id: UUID, action: CounterRowAction) case factPrompt(FactPromptAction) }
— 10:22
And the AppEnvironment doesn’t need to change at all because it already has all of the dependencies the fact prompt needs to do its job.
— 10:33
Next we need to update the appReducer to account for the new fact prompt behavior. We are getting a compiler error right now because we need to handle the new .factPrompt case, so let’s do that real quick: case .factPrompt: return .none
— 10:48
In order to get all of factPromptReducer ’s functionality integrated into the appReducer we need to invoke it from this action. To do that we can start by binding on the action inside the .factPrompt case: case let .factPrompt(factPromptAction):
— 11:03
Now that we a local fact prompt action we can try running the reducer on it. Reducers have a run function that allows you to provide an inout piece of state, an action and an environment: factPromptReducer.run( &state.factPrompt, factPromptAction, FactPromptEnvironment( fact: environment.fact, mainQueue: environment.mainQueue ) ) Value of optional type ‘FactPromptState?’ must be unwrapped to a value of type ‘FactPromptState’
— 11:38
Unfortunately this doesn’t work because the factPromptReducer expects an honest piece of state, not an optional. So, we should first try unwrapping the fact prompt state, and if that fails I guess we can just early out with no effects: guard var factPrompt = state.factPrompt else { return .none }
— 12:03
Now we can run the reducer on this piece of state: factPromptReducer.run( &factPrompt, factPromptAction, FactPromptEnvironment( fact: environment.fact, mainQueue: environment.mainQueue ) )
— 12:07
But this is only going to mutate the little local piece of factPrompt , not the factPrompt sitting inside state . This means we have to remember to further mutate state after the reducer runs: state.factPrompt = factPrompt
— 12:24
Further, running the factPromptReducer returns some effects, which we have to deal with: let effects = factPromptReducer.run( &factPrompt, factPromptAction, FactPromptEnvironment( fact: environment.fact, mainQueue: environment.mainQueue ) )
— 12:31
Unfortunately we can’t just return these effects as-is: return effects Cannot convert return expression of type ‘Effect<FactPromptAction, Never>’ to return type ‘Effect<AppAction, Never>’
— 12:38
Currently the effects can emit FactPromptAction s, but we need them to emit AppAction s. It’s easy enough to do that, we just have to .map on the effects to bundle the actions into an AppAction : .map(AppAction.factPrompt)
— 12:54
And now this compiles, but it would be pretty annoying to have to write this code every time we want to run a reducer on some optional state. Luckily there’s a better way. We can write a higher-order reducer for transforming any reducer into one that operates on optional state. Under the hood it will basically do everything we are doing here, but it will hide away those details in a nice operator.
— 13:21
The Composable Architecture comes with this operator, but we’ll just paste in a simplified version of it: extension Reducer { func optional() -> Reducer<State?, Action, Environment> { .init { state, action, environment in guard var wrappedState = state else { return .none } defer { state = wrappedState } return self.run(&wrappedState, action, environment) } } }
— 13:28
As you can see its sole purpose is to hide away that logic for guard unwrapping the state, running the reducer, and then sticking the new state back in.
— 13:47
You will notice it isn’t doing any of the other work, such as mapping on effects or transforming to local state, actions and environment. But we have another operator that does just those tasks, .pullback , and it can be used in conjunction with this new .optional() operator.
— 14:06
So let’s comment out all the ad-hoc work we are doing in the reducer now.
— 14:15
And instead of doing that we can simply combine the factPromptReducer into the main appReducer just as we are doing with the counterReducer . However, instead of applying .forEach to run the reducer on a collection of state, we will apply .optional() to run the reducer on optional state: factPromptReducer .optional() .pullback( state: \.factPrompt, action: /AppAction.factPrompt, environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) } ),
— 15:22
It’s pretty cool to be able to focus on more high level concepts and constructions, and under the hood it implements all that messiness of dealing with local domains, working around optional state, and mapping effects.
— 15:44
So we now have all of the fact prompt’s logic running inside the app’s logic, but haven’t yet added new logic for creating fact prompt state. It will just stay at nil forever unless we construct it. The moment we want to construct it is when we get a response back from the fact API request. Because our reducers are all composed into a single big reducer it is trivial for the parent to snoop on what is happening in the child.
— 16:12
For example, all we have to do is destructure the full path to reach into the .counterRow , and then reach into the .counter action of that row, then reach into the .factResponse of those actions, and finally reach into the .success case: case let .counterRow( id: id, action: .counter(.factResponse(.success(fact)))) :
— 16:43
This code path will execute whenever we get a response from the fact API. It’s pretty incredible how easy it is for the parent to tap into the logic of the child domain to layer on new functionality.
— 16:50
In this case we want to create some FactPromptState : state.factPrompt = .init(count: <#Int#>, fact: fact)
— 17:02
But to do that we need the current count of the counter being interacted with. To do this we need to reach into the array of counters, find the one corresponding to the id handed to us from the action, and then further traverse into the counter state: let count = state.counters[id: id]?.counter.count
— 17:21
Looking up a counter by its id is failable since we can give an id that doesn’t exist in the array, and so let’s further guard on it, and then we can finally construct the FactPromptState : case let .counterRow( id: id, action: .counter(.factResponse(.success(fact))) ): guard let count = state.counters[id: id]?.counter.count else { return .none } state.factPrompt = .init(count: count, fact: fact) return .none
— 17:37
One final thing. We need to also listen for when the user taps the dismiss button in the fact prompt so that we can close the prompt: case .factPrompt(.dismissButtonTapped): state.factPrompt = nil return .none case .factPrompt: return .none A fact prompt view
— 17:56
OK, that wraps it up for integrating the logic of the application with the fact prompt.
— 18:43
Next we need to figure out how to display the view based on the factPrompt state being nil or non- nil .
— 18:57
Since we want to show the prompt when the factPrompt state is non- nil it sounds reasonable that we would start with an if let to unwrap the state: if let factPrompt = viewStore.factPrompt { … }
— 19:15
And then in here we’d like to pass along a fact prompt store to the view: if let factPrompt = viewStore.factPrompt { FactPrompt( store: <#Store<FactPromptState, FactPromptAction>#> ) }
— 19:23
However, constructing this view requires a store holding onto the fact prompt domain and all we have is a store holding onto all of the app domain. So, sounds like maybe we just want to .scope on the store to focus in on the fact prompt domain: if let factPrompt = viewStore.factPrompt { FactPrompt( store: self.store.scope( state: \.factPrompt, action: AppAction.factPrompt ) ) } Key path value type ‘FactPromptState?’ cannot be converted to contextual type ‘FactPromptState’
— 20:02
However we can’t do that because that would be handing the view a store of optional FactPromptState and it wants honest FactPromptState .
— 20:11
Well, we do have some honest fact prompt state available, in fact it’s the whole reason we got into this branch of the if let in the first place. So perhaps we can open up a custom closure for scoping the state rather than just plucking out the factPrompt field with a key path, and do a little bit of nil coalescing: if let factPrompt = viewStore.factPrompt { FactPrompt( store: self.store.scope( state: { $0.factPrompt ?? factPrompt }, action: AppAction.factPrompt ) ) }
— 20:38
And that seems to compile just fine. In fact, I believe if we run the preview everything will work as we expect too. We can add a counter, count up a few times, tap the fact button, and a moment later the fact prompt will show at the bottom. We can also request a few new facts and dismiss the prompt. Everything just works.
— 21:03
But this solution for dealing with optional state in the view isn’t as nice as it could be. For one, it’d be nice to have a helper view that hides away the if let and coalescing details, much like how ForEachStore hides away the details of how to juggle indices under the hood. Further, we are observing all of factPrompt state so that we can try unwrapping the state, but really we only care about when it flips from nil to non- nil or the opposite. But the way it’s built right now we are observing every change in the facePrompt state, which means the AppView is going to recompute its body whenever something changes inside the prompt. That doesn’t seem right.
— 21:49
Both of these problems can be solved by using a tool the Composable Architecture provides called IfLetStore . It’s analogous to using ForEachStore for collections, but it works on optional state instead. It allows you to transform stores that work with optional values into stores that work with honest values.
— 21:59
To construct one you need to provide a store that holds onto optional state, as well as a view function that takes a store of non-optional state and returns some view: IfLetStore( <#Store<_?, _>#>, then: <#(Store<_, _>) -> View#> )
— 22:18
We can scope app store we have to focus in on the optional fact domain, and we can provide the FactPrompt view’s initializer as the content function: IfLetStore( self.store.scope(state: \.factPrompt, action: AppAction.factPrompt), then: FactPrompt.init(store:) )
— 23:18
This short, succinct chunk of code accomplishes the same thing the manual if let did above, but does so in a more efficient manner. If we hop to the implementation of IfLetStore we will see that it uses a WithViewStore under the hood, but it emits only when the value changes from nil to non- nil or vice versa: public var body: some View { WithViewStore( self.store, removeDuplicates: { ($0 != nil) == ($1 != nil) }, content: self.content ) }
— 23:40
The state inside the optional can change as much as it wants and it will not cause the content to be recomputed. Only when the state flips to nil or non- nil will the view be computed, and so this should be a bit more efficient than our naive attempt.
— 23:53
We’ve now got our fact prompt in place and it’s being powered by the .optional higher-order reducer and the IfLetStore view helper. We’ve made a decent number of changes to our feature since we first wrote the test suite for it, so let’s run tests to see what has changed: State change does not match expectation: … AppState( counters: [ CounterRowState( counter: CounterState( − alert: Alert( − message: "1 is a good number.", − title: "Fact" − ), + alert: nil, count: 1 ), id: 00000000-0000-0000-0000-000000000000 ), ], − factPrompt: nil + factPrompt: FactPromptState( + count: 1, + fact: "1 is a good number.", + isLoading: false + ) ) (Expected: −, Actual: +)
— 24:14
We get a failure on the step where we assert that we received a response from the number fact API, and that’s because we are now longer showing an alert with the fact but rather we are showing a prompt at the bottom of the screen. So, to fix this assertion we need to describe the piece of state that drives that prompt: store.receive( .counterRow( id: id, action: .counter( .factResponse(.success("1 is a good number.")) ) ) ) { $0.factPrompt = .init(count: 1, fact: "1 is a good number.") // $0.counters[id: id]?.counter.alert = .init( // message: "1 is a good number.", title: "Fact" // ) }
— 24:49
Further, we will no longer need to dismiss the alert but rather we will tap the dismiss button in the fact prompt: // store.send( // .counterRow(id: id, action: .counter(.dismissAlert)) // ) { // $0.counters[id: id]?.counter.alert = nil // } store.send(.factPrompt(.dismissButtonTapped)) { $0.factPrompt = nil }
— 25:08
And now we have a passing test suite. It’s pretty awesome how tightly integrated all of our features are. We are simultaneously testing how essentially 3 separate features speak to each other. We’ve got the counter feature, which is responsible for counting logic and making the fact API response. We’ve also got the app feature which intercepts the fact response from a particular row in the list, and then shows the fact prompt at the bottom of the screen, and it also dismisses the prompt. And finally we’ve got the fact prompt which handles loading additional facts. All three of those domains seamlessly interact with each other, and we can exhaustively test the whole package. Enum state
— 25:47
So this is pretty cool. Just as the Composable Architecture gave us tools for transforming reducers and stores that deal with collections of state, it also gives us the tools to deal with optional state. This makes it very easy to show and hide a view depending on the optionality of some state, and further makes it easy to pass a store of behavior to that view.
— 26:08
The fact that we could pass a store of behavior to the FactPrompt view is crucial in being able to break down large complex, domains into small, understandable ones. This allows the FactPrompt to fully encapsulate its behavior without ever affecting the parent. It can start to implement lots of new features, lots of new effects, and introduce more state for it to do its job, all without ever letting that complexity infect the app domain. Further, we can even separate the entire fact prompt domain and view into its own module, making it very clear what its responsibilities are.
— 26:42
So we now have tools for dealing with collections of states and optional state, but there’s still one glaring omission: enums. Enums are just a generalization of optionals, so we would hope that there is an easy way to adapt the IfLetStore view to work with any enum. But unfortunately that isn’t the case. We need to do a little bit of extra work to deal with enum state, and if we put in a little bit of extra work we can build a really robust tool. Unfortunately the library does not currently have these tools provided to us, so we are going to build them from scratch right now in this episode.
— 27:15
To explore the idea of enum state we are going to turn to an example project in the Composable Architecture repository. As everyone probably knows, the repo contains a bunch of demo applications and case studies to demonstrate how to solve many common problems.
— 27:30
We will be looking at the Tic-Tac-Toe demo, which shows how to build a multi-screen application in the Composable Architecture, including a simple, albeit contrived, login flow with two factor authentication. At the root of the application we hold some state that determines whether we are in the logged-out or logged-in state. Since we don’t currently have the tools for dealing with enum state in the library we turned to a hack in order to approximate enums.
— 28:06
Here we are in the Tic-Tac-Toe demo application. We can take things for a spin to get a feel for the application, which is written to work with both SwiftUI and UIKit. We can go through the SwiftUI flow, which starts with a login flow, and once we log in we land on a new game screen, and we can quickly play a game.
— 29:07
If we look at AppCore.swift we will see that the main app state for the entire application looks like this: public struct AppState: Equatable { public var login: LoginState? = LoginState() public var newGame: NewGameState? public init() {} }
— 29:17
We have two pieces of state, both optional, one for when we are in the log in flow and the other for when we are in the new game flow. Using two optionals is a classic way to approximate enums in languages that don’t have proper support for enums. However, it leaves us open to have invalid states, such as when both are nil or both are non- nil . Those situations wouldn’t make any sense for our application, and ideally we would like to make those invalid states unrepresentable by the compiler.
— 29:55
This is a concept we talked about a bunch on just our 4th episode of Point-Free, where we explored the connection between Swift’s type system and algebra. In those episodes we saw that in languages without proper support for enums we can only approximate the concept with lots of optionals, but doing so opens you up to lots of states that make no sense at all.
— 30:17
So, ideally we’d like to be able to take full advantage of Swift’s enums feature to properly model AppState like this: public enum AppState: Equatable { case login(LoginState) case newGame(NewGameState) public init() { self = .login(.init()) } }
— 30:44
But this complicates using the operators we have defined on reducers and stores that aid us in transforming parent domains into child domains.
— 30:55
For example, since the state is currently modeled as a struct with a couple of simple we can take advantage of the .optional() operator and .pullback just as we did with the fact prompt in our previous demo: public let appReducer = Reducer< AppState, AppAction, AppEnvironment > .combine( loginReducer .optional() .pullback( state: \AppState.login, action: /AppAction.login, environment: { LoginEnvironment( authenticationClient: $0.authenticationClient, mainQueue: $0.mainQueue ) } ), newGameReducer .optional() .pullback( state: \.newGame, action: /AppAction.newGame, environment: { _ in NewGameEnvironment() } ), … )
— 31:25
This brings all of the logic from the login domain and new game domain into the app reducer. Key path cannot refer to static member ‘login’ Key path cannot refer to static member ‘newGame’
— 31:35
But if we change AppState to the enum style rather than the struct, we run into some problems. We no longer have a key path to pullback along to get the loginReducer into the appReducer ’s domain. Similar for the newGameReducer . So looks like the library is missing a tool for this situation.
— 31:58
Similarly the view has some problems. If we hop over to AppSwiftView.swift we will see that the AppView body is just two IfLetStore s sitting next to each other: public var body: some View { IfLetStore( self.store.scope(state: \.login, action: AppAction.login) ) { store in NavigationView { LoginView(store: store) } .navigationViewStyle(StackNavigationViewStyle()) } IfLetStore self.store.scope(state: \.newGame, action: AppAction.newGame) ) { store in NavigationView { NewGameView(store: store) } .navigationViewStyle(StackNavigationViewStyle()) } }
— 32:20
This previously worked because we could easily scope down to the optional login or newGame domain, apply IfLetStore to unwrap the state, and then pass the transformed store to the LoginView or NewGameView . Now things are more complicated. We need to do extra work to try to extract login or new game state from the AppState enum, which is possible, but we would further like if this view could somewhat mimic the scenario we are in of wanting to destructure an enum. Just as the IfLetStore and ForEachStore kind of mimic if let s and ForEach s, it would be nice if there was some kind of helper that mimicked switching over enums, perhaps called a SwitchStore . This is yet another tool missing from the Composable Architecture.
— 33:02
Well, let’s start building the tools. It’s clear that these tools would be handy, and it’s even possible to build them outside the library because they don’t need access to anything internal, and so there’s no reason to not start exploring these tools right here in the Tic-Tac-Toe target.
— 33:19
Let’s start with the reducer side. We’ll flip back to AppCore.swift . Currently this file isn’t compiling because the pullbacks don’t work. Pullbacks currently require state key paths so that they can extract the login state, reducer on it, and then plug it back into the parent domain. We no longer have key paths since we switched to an enum for the state.
— 33:41
One thing we could do is introduce computed properties on AppState that allow us to mimic key paths. For example, if we just paste in these computed properties for getting and setting login state and new game state into AppState , which is a concept we previously explored : public enum AppState: Equatable { case login(LoginState) case newGame(NewGameState) init() { self = .login(.init()) var login: LoginState? { get { guard case let .login(state) = self else { return nil } return state } set { guard let newValue = newValue else { return } self = .login(newValue) } } var newGame: NewGameState? { get { guard case let .newGame(state) = self else { return nil } return state } set { guard let newValue = newValue else { return } self = .newGame(newValue) } } }
— 34:17
The pullbacks suddenly start compiling again.
— 34:28
However, this isn’t ideal for a few reasons. First this is a bunch of boilerplate to maintain each time we want to use an enum in our state. It’s annoying code to maintain, and it’s not entirely clear that it’s even the correct boilerplate. There is a slight variation of this code we could maintain where we force self to be of the correct case before we allow overwriting it in the setter: set { guard let newValue = newValue, case .login = self else { return } self = .login(newValue) }
— 35:12
This might technically be a little more correct, but it’s tricky and it’s a bummer that each time we write this boilerplate we have to remember what style of setting is the correct one.
— 35:23
Further, by exposing these little helper computed properties we are making this way of interacting with the enum a part of its public API. However, this is not the best way to interact with this enum. By not being explicit with how we destructure the enum and instead taking a short cut we are potentially opening ourselves up to subtle bugs. It makes it easy for us to write large swaths of code that optionally operate on the state in a single case, while silently going into the void if our state is not currently in that case. It is far safer to explicitly switch on the enum and deal with each case individually.
— 36:04
So, this is not the direction we want to go.
— 36:12
But what are we to do? Pullbacks require the state transformation to be key paths and we just don’t have any key paths.
— 36:18
Well, we may not have key paths, but we do have case paths, which is the analogous concept to key paths except they are defined on enums. Where key paths allow you pick apart the structure of a struct by focusing on just a single field, case paths allow you to pick apart the structure of an enum by focusing on just a single case. Perhaps we can define a version of pullback that works for state case paths instead of key paths.
— 36:43
Let’s start by getting a signature in place: extension Reducer { func pullback<GlobalState, GlobalAction, GlobalEnvironment>( state toLocalState: CasePath<GlobalState, State>, action toLocalAction: CasePath<GlobalAction, Action>, environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment ) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment> { }
— 37:59
This is identical to the signature of pullback in the library, except the state argument is a CasePath instead of a WritableKeyPath .
— 38:00
Let’s try to implement this function by taking it one step at a time. To begin with, we know we need to return a reducer that operates on global state, global actions and global environment, so we can start there: .init { globalState, globalAction, globalEnvironment in }
— 38:12
Our next step is similar to what we do in a regular pullback , which is try to extract a local action from the global one using the action case path given to us, and if that fails it means we are dealing with an action that doesn’t interest us, and so we can early return: guard let localAction = toLocalAction.extract(from: globalAction) else { return .none }
— 38:54
Next we can do something that differs from the regular pullback , but is actually similar to what we did for the .optional() operator. We will try to extract the local state from the global state, using the state case path, and if that fails we will early return: guard var localState = toLocalState.extract(from: globalState) else { return .none }
— 39:23
Now that we have local state and a local action we can run the local reducer, which is self , and we need to make sure to map the effects to embed any local actions emitted into a global action: self.run( &localState, localAction, toLocalEnvironment(globalEnvironment) )
— 39:45
Running this reducer mutates the local state, but that piece of state is fully separate from global state, which is the thing we actually need to mutate if we want this reducer to have any effect on our application state. So just like the .optional() operator we gotta make sure to mutate the global state: globalState = toLocalState.embed(localState)
— 40:18
And then finally we can return the effects produced by the reducer: let effects = self.run( … ) .map(toLocalAction.embed) … return effects
— 40:42
That’s all it takes to define a pullback operator that works on state case paths.
— 40:46
It may seem a little strange that we are having to define a whole new pullback just to handle the situation that we want to pull back along a state case path. After all, what if some day we also wanted to pullback along an action key path? Would we need yet another overload? That sounds like a pain, and luckily in future Point-Free episodes we will be able to unify these operations into just a single one.
— 41:10
But, with this new pullback defined we can update the appReducer to pullback the login and new game reducers along case paths instead of key paths, and even better we not get to drop the .optional() operator since we can plainly see that the case path pullback already has the essence of the .optional() operator already baked into its implementation: public let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine( loginReducer // .optional() .pullback( state: /AppState.login, action: /AppAction.login, environment: { LoginEnvironment( authenticationClient: $0.authenticationClient, mainQueue: $0.mainQueue ) } ), newGameReducer // .optional() .pullback( state: /AppState.newGame, action: /AppAction.newGame, environment: { _ in NewGameEnvironment() } ), … )
— 41:38
Now this part of the reducer is compiling, but something down below is having troubles.
— 41:53
This is where having our AppState properly modeled as an enum instead of a struct really starts to pay dividends. There’s some logic in this reducer that handles when it’s time to transition to the new game screen and when it’s time to transition back to the login screen.
— 42:08
For example, when we get a .success response from the two factor API call or login API call we know it’s time to flip over to the new game screen, and so we create some NewGameState and nil out the login state: case let .login(.twoFactor(.twoFactorResponse(.success(response)))), let .login(.loginResponse(.success(response))) where !response.twoFactorRequired: state.newGame = NewGameState() state.login = nil return .none
— 42:17
This is some really tricky code. We have to be quite familiar with the inner workings to know that in order to transition to the new game screen we have to also nil out the login state. If we forget to do that we will accidentally have non- nil login and new game state at the same time, and that’s completely nonsensical and will lead to both views being visible at once on the screen.
— 42:45
By having AppState be an enum instead of struct we make this invalid state completely impossible, verified by the compiler. All we have to do is overwrite state to be the .newGame case of the enum, and that automatically ensures that the login state has been cleared out: // state.newGame = NewGameState() // state.login = nil state = .newGame(.init())
— 43:14
That is much better.
— 43:15
Similarly, down below when we detect that the logout button has been tapped, we are currently nil -ing out the new game state and then creating some fresh login state to represent transitioning back to login. We no longer need to juggle these independent pieces of state because it’s all been unified in the AppState enum, allowing us to just overwrite all of state with the .login case: case .newGame(.logoutButtonTapped): // state.newGame = nil // state.login = LoginState() state = .login(.init()) return .none
— 43:43
So this is pretty amazing. We are already seeing how using a proper model for our app state is allowing us to clean up application logic and make potential future bugs impossible.
— 43:55
This file is now compiling, and it looks like the only file having trouble now is AppSwiftView.swift . Now there is one thing we could do to get this file compiling without too much extra work. We can no longer just pluck out .login state from AppState because it’s no longer a struct, but we can use case paths to optionally extract login state from the AppState enum: IfLetStore( self.store.scope( state: (/AppState.login).extract(from:), action: AppAction.login ) ) { store in NavigationView { LoginView(store: store) } .navigationViewStyle(StackNavigationViewStyle()) } And we can do the same for the new game view: IfLetStore( self.store.scope(state: (/AppState.newGame).extract(from:), action: AppAction.newGame) ) { store in NavigationView { NewGameView(store: store) } .navigationViewStyle(StackNavigationViewStyle()) }
— 45:04
We can even shorten this code because, just as Swift allows you to use key path expressions in place of getter functions, case path expressions can be used in place of extract functions: IfLetStore( self.store.scope(state: /AppState.login, action: AppAction.login) ) { store in Switch stores
— 45:27
This is pretty cool, we’ve technically solved the problem in both the reducer and the view. We were able to cook up a new pullback for enum state, and we were even able to define it outside of the Composable Architecture library. Then in the view we were able to change some key path getters into case path extractors and things were compiling.
— 46:01
This works, but it’s far from ideal.
— 46:08
First of all there is no indication in this view that what we are actually doing is switching over some state so that we can present a different view for each case. That’s a little superficial, but it can be important for signaling to people who read the code what is trying to be accomplished. It’s easier to understand a view hierarchy that mimics a switch rather than seeing a bunch of IfLetStore s hanging around.
— 46:35
Second, there’s nothing enforcing some of the semantics we expect when we use switch statements, such as allowing only one case to execute and exhaustivity. Currently if we copy and paste one of these IfLetStore s we’ll just get two versions of the view rendering at the same time, and if we leave one off the application will continue humming along just fine.
— 46:59
What if we could cook up some view helpers, much like the IfLetStore and ForEachStore , that are tuned specifically for switching and handling cases. This is an idea that first originated in the community. A GitHub discussion was opened about 4 months ago by one of our viewers, Luke Redpath, theorizing what a view for “switching” over a store could look like, and we have that discussion linked at the bottom of our episode page in case you want to check out some of the early experiments in this area.
— 47:26
For example, you could start by constructing something called a SwitchStore that transforms a store that holds onto an enum of state: SwitchStore(self.store) { }
— 47:47
Then, inside this SwitchStore you could provide CaseLet views that allow you to destructure specific cases of the enum, and they would transform the store into one that deals with just the domain of the case you are interested in: SwitchStore(self.store) { CaseLet(state: /AppState.login, action: AppAction.login) { loginStore in } }
— 48:18
And then inside each of these CaseLet views you could construct the login and new game views since we now have stores in the exact shape that is required: SwitchStore(self.store) { CaseLet( state: /AppState.login, action: AppAction.login ) { loginStore in NavigationView { LoginView(store: loginStore) } .navigationViewStyle(StackNavigationViewStyle()) } }
— 48:29
And we could add another CaseLet view for the new game case. CaseLet( state: /AppState.newGame, action: AppAction.newGame ) { newGameStore in NavigationView { NewGameView(store: newGameStore) } .navigationViewStyle(StackNavigationViewStyle()) }
— 48:46
We are handing CaseLet a case path because that’s what allows us pick apart an enum to check if it’s a particular case, much like how key paths can pick apart a struct to focus on a particular field.
— 48:58
Well, it’s possible to build these views, and it’s even possible to bake in some safety checks to make this construction as concise as possible. Or at least as concise as we know how. So, let’s get started.
— 49:11
Let’s comment out this theoretical syntax so that we can get back into compiling order, and let’s go ahead and comment out the IfLetStore stuff since we know we want to find a better way to build these views.
— 49:30
The SwitchStore is going to be a SwiftUI view that holds onto a store, and so at the very least it should be generic over State and Action , where we expect the State to be an enum that we want to destructure: struct SwitchStore<State, Action>: View { let store: Store<State, Action> var body: some View { } }
— 49:51
As we can see from the theoretical SwitchStore syntax above we want to also the view to take a view closure that returns some view. The way you typically do this in SwiftUI is to make the view generic over a Content parameter that conforms to the View protocol and then hold onto a closure that returns a Content value: struct SwitchStore<State, Action, Content: View>: View { let store: Store<State, Action> let content: () -> Content var body: some View { } }
— 50:21
The body can even be implemented to just return that content provided to us: var body: some View { self.content() }
— 50:29
And with that little bit of code we can already construct SwitchStore views, but they won’t do anything interesting: SwitchStore(self.store) { }
— 50:41
But for this to compile we need a custom initializer: init( _ store: Store<State, Action>, @ViewBuilder content: @escaping () -> Content ) { self.store = store self.content = content }
— 51:21
Next we need to implement a CaseLet view that can handle the job of checking if the enum state is in a particular case, and if it is it will render a view for that case.
— 51:31
Recall that we previously theorized that the CaseLet view would be created with a case path and action transformation like: CaseLet( state: /AppState.login, action: AppAction.login ) { loginStore in
— 51:40
This would allow CaseLet to further scope the store that we are switching on to narrow it down to the domain of just a single case. So, let’s sketch out a CaseLet view that holds onto this information. It’ll be a view: struct CaseLet: View { var body: some View { } }
— 5:57
In order to hold onto a state case path we need to know the global state, which is the enum, and the local state, which is the associated value for a particular case, and so that means we need to introduce some generics: struct CaseLet<GlobalState, LocalState>: View { let state: CasePath<GlobalState, LocalState> … }
— 52:19
In order to hold onto the action transformation we also need to know the global action, which are the actions pertinent to the domain of the enum we are switching on, and local action, which are the actions pertinent to the domain of a particular case in the enum: struct CaseLet< GlobalState, GlobalAction, LocalState, LocalAction >: View { let state: CasePath<GlobalState, LocalState> let action: (LocalAction) -> GlobalAction … }
— 52:46
The CaseLet will also be provided a closure for constructing a view from a store on the local domain, which means we need to introduce another generic for the type of view that is shown in this case: struct CaseLet< GlobalState, GlobalAction, LocalState, LocalAction, Content: View >: View { let state: CasePath<GlobalState, LocalState> let action: (LocalAction) -> GlobalAction @ViewBuilder let content: (Store<LocalState, LocalAction>) -> Content … }
— 53:21
We’re getting close to being able to implement the body of this view, but we don’t actually have any data to act upon. We have some transformations for state and actions, and we have a function for constructing views, but we don’t have anything that we can actually feed to these transformations.
— 53:40
We need to further hold onto a store of the global domain, meaning the domain of the enum state: struct CaseLet< GlobalState, GlobalAction, LocalState, LocalAction, Content: View >: View { let store: Store<GlobalState, GlobalAction> let state: CasePath<GlobalState, LocalState> let action: (LocalAction) -> GlobalAction @ViewBuilder let content: (Store<LocalState, LocalAction>) -> Content … }
— 53:50
With that available to us we can now implement the body by constructing an IfLetStore that attempts to extract a case from the global state, and if that succeeds we can show the content view: var body: some View { IfLetStore( self.store.scope(state: state.extract(from:), action: action), then: self.content ) }
— 54:48
Now let’s try constructing one of these CaseLet views inside the SwitchStore . Looks like in order to do so we need to provide 4 arguments: CaseLet( store: <#Store<_, _>#>, state: <#CasePath<_, _>#>, action: <#(_) -> _#>, content: <#(Store<_, _>) -> _#> )
— 55:08
The store argument represents the the store that holds onto the enum we are switching over. It’s a little weird we have to put in here, after all it’s exactly what is provided to the SwitchStore above: SwitchStore(self.store) { CaseLet( store: self.store, state: <#CasePath<_, _>#>, action: <#(_) -> _#>, content: <#(Store<_, _>) -> _#> ) }
— 55:12
The state is the case path that plucks out a particular case from AppState , so let’s start with the .login case: CaseLet( store: self.store, state: /AppState.login, action: <#(_) -> _#>, content: <#(Store<_, _>) -> _#> )
— 55:18
The action transformation will be the function that embeds login actions into app actions: CaseLet( store: self.store, state: /AppState.login, action: AppAction.login, content: <#(Store<_, _>) -> _#> )
— 55:24
Finally we have the content argument to provide. It’s a closure that takes a store of the LoginState and LoginAction and then needs to return a view. We can open up this closure and provide the NavigationView with the LoginView inside it: SwitchStore(self.store) { CaseLet( store: self.store, state: /AppState.login, action: AppAction.login ) { loginStore in NavigationView { LoginView(store: loginStore) } .navigationViewStyle(StackNavigationViewStyle()) } }
— 55:38
And with a copy and paste along with a few small edits we can do the same for the .newGame case of the AppState enum: SwitchStore(self.store) { CaseLet( store: self.store, state: /AppState.login, action: AppAction.login ) { loginStore in NavigationView { LoginView(store: loginStore) } .navigationViewStyle(StackNavigationViewStyle()) } CaseLet( store: self.store, state: /AppState.newGame, action: AppAction.newGame ) { newGameStore in NavigationView { NewGameView(store: newGameStore) } .navigationViewStyle(StackNavigationViewStyle()) } }
— 55:48
And just like the app is compiling, and it should work exactly as it did before, even though we have completely refactored the application state to be an enum rather than two optionals.
— 55:52
Let’s run the app in the simulator just to be sure. Environment objects
— 56:26
So, this is really promising, but there is still a lot of room for improvement.
— 56:32
First of all, it’s really strange to have to pass the store to both the SwitchStore and the CaseLet . In fact, currently the SwitchStore isn’t doing any additional work or logic to render its body, it just calls down to the content closure given to it. This means technically we could comment out the SwitchStore wrapping the CaseLet s and everything would work just the same.
— 56:49
However, there are benefits to keeping the SwitchStore around. By having this wrapper view we can accomplish a few things that would not be possible if we put in a bunch of CaseLet views at the root:
— 57:01
First of all, using SwiftUI’s environment we can have the store from the SwitchStore automatically passed down to all of the CaseLet s so that we don’t pass it in to multiple views. So already having a wrapper view will help clean up that a bit.
— 57:14
Further, we can implement some features that allow us to approximate the idea of exhaustive switching on the enum. Now we can’t have compile time exhaustivity because only switch statements in Swift are afforded that power, but we can provide some runtime exhaustivity checks. Essentially, we can detect if the state changes to a case that we haven’t supported inside the SwitchStore we can either print something to the logs to notify the user, or we could add a precondition, or we could even force a breakpoint.
— 57:42
And if that wasn’t enough, we can make the body of SwitchStore more strict so that it only allows CaseLet s in the body, not just any random view. We can even support “default” views that act as a catch all for any case not handled, and even statically force that view to be at the end of the SwitchStore body.
— 57:59
So, let’s start implementing some of those features.
— 58:03
We’ll start with the strangeness of needing to pass the store to each of the CaseLet views even though the SwitchStore has the store. SwiftUI has a feature known as “environment objects”, which allows you to pass an object all the way through a view hierarchy in an implicit fashion. Basically, you set an environment object on a view, and then every child view in the hierarchy will automatically get access to that object.
— 58:34
We want to take advantage of this by setting an environment object inside the SwitchStore view, and then magically each CaseLet view will get access to it without us having to explicitly pass it through.
— 58:44
We can do this by first setting an environment object from the body of the SwitchStore : public var body: some View { self.content() .environmentObject(self.store) }
— 58:55
Unfortunately we get a compiler error: Instance method ‘environmentObject’ requires that ‘Store<State, Action>’ conform to ‘ObservableObject’
— 58:57
Looks like that in order for us to pass an object through the environment it must conform to the ObservableObject protocol, but Store does not conform to that protocol. If you want to observe the state inside a Store we force you to construct a ViewStore , and we make you perform this extra step because often you want to only observe a small subset of the state inside the store, and so the ViewStore is the perfect place to do this.
— 59:26
Well, what are we to do? If Store doesn’t conform to ObservableObject , how can we pass it through the view hierarchy? Well, environment objects conform to this protocol because changes to the object are supposed to invalidate the hierarchy, allowing child views to react to the changes. However, we don’t actually need that power. We don’t need to update the child views when the store updates because they are observing the store themselves. We just need to get the store to them, period.
— 59:53
Given that insight we can write a wrapper for Store that conforms to ObservableObject but that is completely inert and never publishes changes. We can even make it private because this is just an implementation detail that no one else ever needs to know about, or should depend on: private class StoreWrapper<State, Action>: ObservableObject { let store: Store<State, Action> init(store: Store<State, Action>) { self.store = store } }
— 1:00:41
Then, this is the object we can pass through the environment: public var body: some View { self.content() .environmentObject(StoreWrapper(store: self.store)) }
— 1:00:50
Next, in order to capture this environment variable from inside the CaseLet view we need to change the declaration of the store variable so that it accesses its value from the environment: @EnvironmentObject private var storeWrapper: StoreWrapper<GlobalState, GlobalAction>
— 1:01:14
And then we can access that store inside the wrapper for the IfLetStore : IfLetStore( self.storeWrapper.store.scope( state: state.extract(from:), action: action ), then: self.content )
— 1:01:19
And now we can finally drop the store argument when constructing a CaseLet view because it’s automatically passed to us through the environment: CaseLet( state: /AppState.login, action: AppAction.login ) { loginStore in NavigationView { LoginView(store: loginStore) } .navigationViewStyle(StackNavigationViewStyle()) } CaseLet( state: /AppState.newGame, action: AppAction.newGame ) { newGame in NavigationView { NewGameView(store: newGame) } .navigationViewStyle(StackNavigationViewStyle()) }
— 1:01:36
One interesting thing about making the StoreWrapper private is that it effectively means that CaseLet can’t be used outside the SwitchStore view. If it is it will just immediately crash since the environment object cannot be provided to it. Statically safer switch stores
— 1:01:54
OK, that’s a pretty big improvement, but we can go further.
— 1:01:58
Right now we can interleave views between the CaseLet s, and that doesn’t make a lot of sense. If you want something to show before or after a CaseLet view you are better off putting that inside the body of the CaseLet , not between the CaseLet s themselves.
— 1:02:31
Well, amazingly it’s possible to enforce this statically by the compiler. We can restrict the ways in which you can construct a SwitchStore to force that only CaseLet views provided inside the body of the SwitchStore .
— 1:02:44
In order to do this we need to adapt the initializer for SwitchStore such that you are forced to provide a content closure with a very specific form. We want to somehow say that the view returned from content is of the form of two CaseLet views side-by-side: init( _ store: Store<State, Action>, @ViewBuilder content: @escaping () -> (CaseLet, CaseLet) )
— 1:03:08
This of course doesn’t work for a number of reasons. First of all CaseLet has generics we need to introduce, and the content closure needs to return a SwiftUI view but right now it’s returning a tuple.
— 1:03:17
To understand what’s the exact static type that is returned here we need to understand how SwiftUI and view builders translate two views that are grouped into a scope. For example, if we defined a little throw away view to group two Text views: let tmp = Group { Text("") Text("") }
— 1:03:34
We will find that this value has the type: let tmp: Group<TupleView<(Text, Text)>>
— 1:03:48
So secretly, under the hood, SwiftUI and view builders have transformed this group of Text views into what they call a TupleView , which has a single generic that is a tuple of the views contained in the group.
— 1:03:56
This is the static view we can use to prescribe that CaseLet views must be provided to us in the content closure of the SwitchStore : init( _ store: Store<State, Action>, @ViewBuilder content: @escaping () -> TupleView<(CaseLet, CaseLet)> ) { self.store = store self.content = content }
— 1:04:15
Now let’s introduce the generics necessary to support these CaseLet s. We’ll need generics for the state and actions for the domain of the case as well as the content view for that case: init<State1, Action1, State2, Action2, Content1, Content2>( _ store: Store<State, Action>, @ViewBuilder content: @escaping () -> TupleView<( CaseLet<State, State1, Action, Action1, Content1>, CaseLet<State, State2, Action, Action2, Content2> )> ) { self.store = store self.content = content }
— 1:04:57
This unfortunately doesn’t compile because the types for content and self.content do not match. The content closure passed to the initializer has this TupleView type, but self.content ’s type comes from the generic of the SwitchStore . We can reconcile these types by adding a generic constraint to the initializer that forces the Content generic to be a TupleView : init<State1, Action1, State2, Action2, Content1, Content2>( _ store: Store<State, Action>, @ViewBuilder content: @escaping () -> TupleView<( CaseLet<State, Action, State1, Action1, Content1>, CaseLet<State, Action, State2, Action2, Content2> )> ) where Content == TupleView<( CaseLet<State, Action, State1, Action1, Content1>, CaseLet<State, Action, State2, Action2, Content2> )> { self.store = store self.content = content }
— 1:05:26
Now this compiles, and if we try to insert some additional views between the CaseLet s we’ll get errors. Runtime exhaustivity
— 1:05:50
So this is pretty cool, but of course this initializer only works for two case enums. To handle more cases for larger enums we will need to add some overloads, which is easy enough to do so we won’t do it right now.
— 1:06:15
The next interesting feature we can add to SwitchStore is to support runtime exhaustivity. It’d be awesome if we could make this compile-time exhaustivity so that you get a compiler error if you do not handle every case of the switch , but unfortunately this isn’t possible with Swift today.
— 1:06:43
But, what we can do today is detect if none of the CaseLet views recognized the enum, and in that case we can raise some kind of error to bring it to the attention of the developer. This error could range from anything to a simple message logged to the console or a fatal error to stop execution of the application immediately. We’ll go with a happy medium, which is we will pause the application using a
SIGTRAP 1:07:10
So, let’s get started. Right now our SwitchStore ’s body is specified by the content closure provided, which is just a TupleView that places a bunch of CaseLet s into the view hierarchy. The CaseLet s themselves are IfLetStore s, which means only one of them should be rendering a non-empty view at a time. What we need to do is add some additional logic in the content closure so that we can insert a CaseLet view only if we know for sure that its case matches the enum state of the store.
SIGTRAP 1:07:55
To get access to the state enum we need to observe the store, and so we can start by opening up the content closure and constructing the WithViewStore : self.content = { WithViewStore(store) { viewStore in } }
SIGTRAP 1:08:20
WithViewStore also requires that State is equatable, so just to get things compiling, let’s throw in that constraint, as well: State: Equatable
SIGTRAP 1:08:30
Now this breaks the initializer because self.content is no longer a closure that returns a tuple view, but instead returns a WithViewStore . So, we need to adapt our generic constraint to match the view hierarchy we build in this closure: where Content == WithViewStore<State, Action, EmptyView>
SIGTRAP 1:08:53
The third generic is the type of view that we construct inside the WithViewStore , so we will have to update it as we build out more of the view.
SIGTRAP 1:09:01
Once we are inside the WithViewStore we have access to the state enum, which means we can start checking the cases provided to us in the SwitchStore to see which match. To get access to each case provided to us we can invoke the content closure and destructure the tuple into each individual CaseLet view: self.content = { let (case1, case2) = content().value WithViewStore(store) { viewStore in } }
SIGTRAP 1:09:43
Now that we have access to each CaseLet view we can reach into those views to grab their case paths, and use that to check if the case matches. We can even use a recent new feature of case paths to make this succinct. We recently implemented the ~= operator on case paths that allows you to use case paths to destructure values in a switch : public func ~= <Root, Value>( pattern: CasePath<Root, Value>, value: Root ) -> Bool
SIGTRAP 1:10:12
With this operator defined we can simply switch on viewStore.state and then try matching on the case path from each CaseLet view: return WithViewStore(store) { viewStore in switch viewStore.state { case case1.state: case case2.state: default: } }
SIGTRAP 1:10:35
If one of the first two case paths match we will just return the case view: return WithViewStore(store) { viewStore in switch viewStore.state { case case1.state: case1 case case2.state: case2 default: } }
SIGTRAP 1:10:44
And then for the default is where we can put the runtime exhaustivity check. We’ll want to put a view in this spot that triggers a breakpoint if it is ever initialized, and it doesn’t really matter what we put in the body of the view, but we might as well make it stand out as a big red warning: struct BreakpointView: View { init() { fputs( """ ⚠️ Warning: SwitchStore must be exhaustive. A SwitchStore was used without exhaustively handling every \ case with a CaseLet view. Make sure that you provide a \ CaseLet for each case in your enum. """, stderr ) raise(SIGTRAP) } var body: some View { #if DEBUG Text( """ ⚠️ Warning: SwitchStore must be exhaustive. A SwitchStore was used without exhaustively handling every \ case with a CaseLet view. Make sure that you provide a \ CaseLet for each case in your enum. """ ) .frame(maxWidth: .infinity, maxHeight: .infinity) .foregroundColor(.white) .background(Color.red.ignoresSafeArea()) #endif } }
SIGTRAP 1:11:22
And this new BreakpointView is exactly what we will put in the default case of our switch : switch viewStore.state { case case1.state: case1 case case2.state: case2 default: BreakpointView() }
SIGTRAP 1:11:27
The initializer is no longer compiling, but that’s just because we have to figure out how to update the generic constraint so that it matches what we are doing inside the WithViewStore . The compiler error message even helpfully lets us know what the type is: Cannot convert value of type ‘_ConditionalContent<_ConditionalContent<CaseLet<State, Action, State1, Action1, Content1>, CaseLet<State, Action, State2, Action2, Content2>>, BreakpointView>’ to closure result type ‘EmptyView’
SIGTRAP 1:11:33
It seems that whenever you do a switch statement in a view builder context it magically gets converted to a nested _ConditionalContent static type. So, we can just copy and paste this in our generic constraint, along with a liberal use of new lines to make it more readable, to get things compiling again: Content == WithViewStore< State, Action, _ConditionalContent< _ConditionalContent< CaseLet<State, Action, State1, Action1, Content1>, CaseLet<State, Action, State2, Action2, Content2> >, BreakpointView > >
SIGTRAP 1:12:07
Now this looks pretty intense and it may seem annoying to construct this massive static type to mimic what is going on in this content closure, but this little bit up upfront work helps us retain as much of the static type information as possible. We could always just erase the type to an AnyView , but doing so can cause SwiftUI to miss out on important optimization opportunities and even break animations.
SIGTRAP 1:12:46
To test out this new runtime exhaustivity checking lets add a third case to our AppState enum so that we can “accidentally” not handle it in the SwitchStore : public enum AppState: Equatable { case login(LoginState) case newGame(NewGameState) case pending … }
SIGTRAP 1:13:01
Once we successfully log in we will switch ourselves to the .pending state instead of the .newGame state: case let .login(.twoFactor(.twoFactorResponse(.success(response)))), let .login(.loginResponse(.success(response))) where !response.twoFactorRequired: state = .pending // .newGame(NewGameState()) return .none
SIGTRAP 1:13:14
Now when we run the app and login we will trigger the breakpoint, allowing us to see the message that was just printed to the console. Further, if we continue we will see that the screen shows a big red view that makes it clear what went wrong.
SIGTRAP 1:13:48
So let’s fix things by backing out of this change. state = .newGame(NewGameState()) Performant switch evaluation
SIGTRAP 1:14:01
There’s one more improvement we want to make to the SwitchStore before moving on. Currently in the SwitchStore we are observing all of state so that we can figure out which case we are on and render the appropriate CaseLet view: return WithViewStore(store) { viewStore in }
SIGTRAP 1:14:16
However, this means that all the changes that happen in the login view or all the changes that happen in the new game view (and their children views) will cause this body to recompute. The only time this should recompute is when the state switches cases, like if we go from a logged in state to a new game state. All the changes that happen within a particular case without changing the actual case of the state won’t have any impact on this switch statement, and so it should be skipped.
SIGTRAP 1:14:43
So, we want to implement the removeDuplicates argument for WithViewStore so that we can filter out any changes that did not result in a change of the case of the state enum: WithViewStore(store, removeDuplicates: { ??? }) { viewStore in }
SIGTRAP 1:15:00
But how can we do that?
SIGTRAP 1:15:06
We need some way of dynamically computing the case of any arbitrary enum value. And what does it mean to “compute the case” anyway? For the purposes of the ViewStore we just need to be able to compute some kind of identifying information from an enum value such that all values in the same case have the same identifier, which would allow us to remove duplicate state emissions when the case of the enum does not change: WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in
SIGTRAP 1:15:36
So, how can we implement this theoretical tag function which turns any enum into an identifier that identifies all values in a single case, and distinguishes cases? Well, we have to resort to some Swift runtime metadata. Every compiled Swift program has lots of metadata embedded in it that can unlock some really powerful things. It’s what SwiftUI uses under the hood to do a bunch of seemingly magical things.
SIGTRAP 1:16:00
Unfortunately we don’t have time to go deep into how one culls information from the depths of the Swift runtime. The APIs and concepts are a bit more low level than we are used to in every day Swift, so we’re just going to paste the solution: private struct EnumValueWitnessTable { let f1, f2, f3, f4, f5, f6, f7, f8: UnsafeRawPointer let size, stride: Int let flags, extraInhabitantCount: UInt32 let getEnumTag: @convention(c) (_ value: UnsafeRawPointer, _ metadata: UnsafeRawPointer) -> UInt32 let f9, f10: UnsafeRawPointer } private func enumTag<Enum>(_ case: Enum) -> UInt32? { let enumType = type(of: case) let metadataPtr = unsafeBitCast(enumType, to: UnsafeRawPointer.self) let metadataKind = metadataPtr.load(as: Int.self) let isEnum = metadataKind == 0x201 guard isEnum else { return nil } let vwtPtr = (metadataPtr - MemoryLayout<UnsafeRawPointer>.size).load(as: UnsafeRawPointer.self) let vwt = vwtPtr.load(as: EnumValueWitnessTable.self) return withUnsafePointer(to: case, { vwt.getEnumTag($0, metadataPtr) }) }
SIGTRAP 1:16:14
That’s intense, but it now implements the enumTag function we need to remove duplicates, and if we run the application again we will see that the body of the SwitchStore is not executed again while we are interacting with the login flow. It’s only executed once we switch from the login state to the new game state.
SIGTRAP 1:17:28
So this is pretty cool. Using some pretty advanced techniques from SwiftUI we have been able to build tools for the Composable Architecture that allow us to embrace better data modeling for our applications. We can now use enums for our state and emulate the idea of destructuring a store by using a SwitchStore view with a bunch of CaseLet views inside. We can even provide some basic support for exhaustivity checking so that if the state gets into a case that is not handled by a CaseLet view we will breakpoint to let the developer know there’s additional state to handle. And on top of all that we’ve even made the view efficient by making sure it recomputes its body if and only if the case of the enum changes.
SIGTRAP 1:18:07
We can even push some of these ideas a bit further. With a little bit of extra work you can also support default-like statements for SwitchStore . This can be handy if you have a lot of cases in your enum and you want to handle only a few while allowing all the others to fall through to a default view. We have some exercises for this episode that will help you explore these ideas. Next time: the point
SIGTRAP 1:18:27
In the past 4 episodes we have really dug deep into the concept of “deriving behavior”, which means how do we take a big blob of “behavior” for our application and break it down into smaller pieces. This is important for building small, understandable units for your application that can be plugged together to form the full application, as well as for modularizing your application which comes with a ton of benefits.
SIGTRAP 1:18:53
We started this exploration by first showing what this concept looks like in vanilla SwiftUI by using ObservableObject s. Apple doesn’t give us direct tools to be used for this problem, but we are able to use some tools from Combine in order to break down large view models into smaller domains. We got it to work, but it wasn’t exactly pretty. We had to do extra work to get the parent domain to update when a child domain changed, and we had to do some trickery to synchronize changes between sibling child domains without introducing memory leaks or infinite loops.
SIGTRAP 1:19:25
Then we turned our attention to the Composable Architecture. We showed that out of the box the library gives us a tool for breaking down large pieces of application logic into smaller pieces, which is the .pullback operator on reducers. And the library gives us a tool for breaking down large runtimes, which is the thing that actually powers our views, into smaller pieces by using the .scope operator on stores. These two tools allowed us to build features in isolation without any understanding of how they will be plugged into the larger application, and then it was trivial to integrate child feature into parent features.
SIGTRAP 1:19:55
Once we got a feeling for how pulling back and scoping work in the Composable Architecture we started flexing those muscles more. We started exploring tools that allow us to embed our domains into a variety of data structures, such as collections, optionals and enums. This includes using reducer operators such as the .forEach operator that allows you to run a reducer on every element of a collection, the .optional operator that enhances a reducer to work on optional state, and even a new version of .pullback that pulls back along state case paths for when your state is an enum. Corresponding to each of those reducer operators were new SwiftUI views for transforming the store, such as the ForEachStore , IfLetStore and even SwitchStore .
SIGTRAP 1:20:22
That was all pretty amazing, but now it’s time to ask: what’s the point? This is our opportunity to try to bring things down to earth and maybe even dig in a little deeper. This time we want to end this series of episodes like we started: we want to show what one must do in vanilla SwiftUI to handle things like collections of domains and optional domains so that we can better understand how it compares to the Composable Architecture and see why it is important to have tools that are tailored for these use cases.
SIGTRAP 1:20:52
So, let’s try building our demo app with the collection of counters and fact banner in vanilla SwiftUI…next time! References GitHub Discussion: CaseLetStore (for example) Luke Redpath • Feb 18, 2021 Earlier this year, one of our viewers, Luke Redpath , started a Composable Architecture GitHub discussion around the creation of a SwitchStore -like view that inspired the design introduced in this episode. https://github.com/pointfreeco/swift-composable-architecture/discussions/388 Composable Architecture Release 0.19.0 Brandon Williams and Stephen Celis • Jun 14, 2021 After publishing this episode we released 0.19.0 of the Composable Architecture, bringing SwitchStore and CaseLet views to all users of the library. https://github.com/pointfreeco/swift-composable-architecture/releases/tag/0.19.0 Downloads Sample code 0149-derived-behavior-pt4 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .