EP 74 · Modular State Management · Sep 30, 2019 ·Members

Video #74: Modular State Management: View Actions

smart_display

Loading stream…

Video #74: Modular State Management: View Actions

Episode: Video #74 Date: Sep 30, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep74-modular-state-management-view-actions

Episode thumbnail

Description

It’s time to fully modularize our app! Our views can still send any app action, so let’s explore transforming stores to focus in on just the local actions a view cares about.

Video

Cloudflare Stream video ID: 9aae262f6f8808d8daad40f2a0c16b47 Local file: video_74_modular-state-management-view-actions.mp4 *(download with --video 74)*

References

Transcript

0:06

And just like that our views now take a smaller subset of state than the full AppState , which means the stores that power the views are starting to look a lot more like the reducers that power them.

0:19

It’s also worth noting how simple these changes were, and how quick we made them. We merely changed the state that the views operated on to focus in on just what they needed, and then we made sure that the stores we passed to those views were transformed in order to pluck out these values. Transforming a store’s action

0:40

We’ve now improved the modularity and ergonomics of accessing state in views that use the observable Store object, but we can’t quite extract any of our views to isolated modules because every view’s store still depends on AppAction . So it sounds like we need another operation to transform stores that send global actions into stores that send local actions. We found that we can transform a store’s value, so can we also transform a store’s action?

1:14

First, what does that mean? Right now the only time we externally deal with actions and the store is when we are sending an action to the store, by calling the send method. We would love if we could send local actions to that method instead of global ones, and then somewhere in the store maybe it can automate the process of wrapping up local actions into the global actions.

1:46

Let’s start by getting another function signature in place for this transformation. We’ll start with another unnamed method, just so we can focus on making the transformation work. final class Store<Value, Action>: ObservableObject { … func ___() }

1:58

And again, we want to return a brand new store. final class Store<Value, Action>: ObservableObject { … func ___() -> Store }

2:01

We want this store to work with actions that are more local than the current store. final class Store<Value, Action>: ObservableObject { … func ___<LocalAction>() -> Store<Value, LocalAction> }

2:20

We can once again open the body of the function and immediately return a Store<Value, LocalAction> . final class Store<Value, Action>: ObservableObject { … func ___<LocalAction>() -> Store<Value, LocalAction> { return Store<Value, LocalAction>( initialValue: <#Value#>, reducer: <#(inout Value, LocalAction) -> Void#> ) } }

2:32

In order to supply an initial value to the store, we can merely pass the current self.value along. The type doesn’t change. initialValue: self.value,

2:44

And now…we just need to figure out how to implement another one of these weird, reducer-like functions. Let’s open up the closure. initialValue: self.value, reducer: { value, localAction in }

2:53

The main bit of work we did in here when we transformed a store’s value was to call the root store’s self.send method and use its updated self.value to update the sub-store’s value accordingly. In order to call self.send in this method, we need an Action , but we only have a LocalAction . If we could somehow transform LocalAction s into Action s, we should be able to implement this. Sounds like another function!

3:13

Let’s introduce another f from LocalAction to Action . func ___<LocalAction>( _ f: (LocalAction) -> Action ) -> Store<Value, LocalAction> {

3:21

And we can now use this f to transform localAction into a more global action before passing it along to the store. reducer: { value, localAction in self.send(f(localAction)) }

3:29

Ah, but we need to mark f escaping since it’s captured by the store. func ___<LocalAction>( f: @escaping (LocalAction) -> Action ) -> Store<Value, LocalAction> {

3:32

And finally, we need to reassign the in-out, mutable value with self.value . reducer: { value, localAction in self.send(f(localAction)) value = self.value }

3:52

Alright, things are compiling, but again it’s a strange reducer. It’s depending on the internal behavior of the store to get its value mutated, and then it reassigns the mutable value even though it didn’t use that value at all.

4:18

But strangeness aside, does this function have a shape that is familiar to us at all?

4:32

Let’s take a look at its signature: ((LocalAction) -> Action) -> ((Store<_, Action>) -> Store<_, LocalAction>)

4:58

Generalize the generic names to A s and B s: ((B) -> A) -> ((Store<A, _>) -> Store<B, _>)

5:18

And generalize the context away from Store to get a very clear picture of things. ((B) -> A) -> (F<A>) -> F<B>)

5:32

This is a function signature we’ve encountered several times before on Point-Free, and we named the operation pullback . pullback: ((A) -> B) -> (F<B>) -> F<A>)

5:38

It’s a function shape we first explored in our deep dive into “ contravariance ,” where we explored a form of composition that reverses function arrows, just as we’re seeing here.

5:53

So we might be tempted to call this operation pullback : public func pullback<LocalAction>( f: @escaping (LocalAction) -> Action ) -> Store<Value, LocalAction> {

6:03

However, for reasons that are similar to what we described for the map operation on Store , we do not feel comfortable calling this method pullback . It has all of the same problems due the fact that it is acting on a reference type and depends on the behavior of this object rather than just the pure, mathematical properties.

6:20

Instead, we might name it “view,” since it returns a view of a store that can only send a local set of actions. final class Store<Value, Action>: ObservableObject { … public func view<LocalAction>( f: @escaping (LocalAction) -> Action ) -> Store<Value, LocalAction> { return Store<Value, LocalAction>( initialValue: self.value, reducer: { value, localAction in self.send(f(action)) value = self.value } ) } } Combining view functions

6:35

And this is basically the function that can transform stores on global actions into stores on more local actions, but it’s not quite there yet. It still needs to handle subscribing local stores to more global ones, and maybe we should combine these two view functions into a single function that does all of the work at once.

6:58

If we paste the functions close together, we can embed all of the action transformations into the method that also handles state transformations. First we can introduce the LocalAction generic, and include both transform functions as input. public func view<LocalValue, LocalAction>( _ f: @escaping (Value) -> LocalValue, _ g: @escaping (LocalAction) -> Action

7:26

And now it becomes very clear, side-by-side, that the transform functions go in completely opposite directions.

7:35

Let’s update the argument names to make things more readable. func view<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action

8:02

And now we need to update all of the calls to our view transform functions, and handle the transformation of local actions into global ones. public func view<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action ) -> Store<LocalValue, LocalAction> { let localStore = Store<LocalValue, LocalAction>( initialValue: toLocalValue(self.value) reducer: { localValue, localAction in self.send(toGlobalAction(localAction)) localValue = toLocalValue(self.value) } ) localStore.cancellable = self.$value .sink { [weak localStore] newValue in localStore?.value = toLocalValue(newValue) } return localStore } Focusing on favorite primes actions

8:45

Our app is no longer compiling because wherever “ContentView.swift” uses Store ‘s view function it is only passing a single transform function for the value. Let’s get things compiling again by passing a transform function for each action. Because all of our views expect stores that work with AppAction , we can use the identity function, spelled { $0 } . Where we initialize the counter screen, we can make the following change: NavigationLink( "Counter demo", destination: CounterView( store: self.store.view( value: { ($0.count, $0.favoritePrimes) }, action: { $0 } ) ) )

9:12

And where we initialize the favorite primes screen, we can make the following change. NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view( value: { $0.favoritePrimes }, action: { $0 } ) ) )

9:25

And finally, where prime modal views are constructed, we can update its call to view as well: IsPrimeModalView( Store: self.store .view( value: { ($0.count, $0.favoritePrimes) }, action: { $0 } ) )

9:39

Alright, things are building again, but our views are still working with global AppAction s. Let’s update our views to work with more local actions instead.

9:55

We can start with the FavoritePrimesView : struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], AppAction>

9:58

And update it to work with FavoritePrimesAction instead: struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], FavoritePrimesAction>

10:08

That broke our mutation: .onDelete { indexSet in self.store.send(.favoritePrimes(.deleteFavoritePrimes(indexSet))) self.store.send(.counter(.incrTapped)) } Type ‘FavoritePrimesAction’ has no member ‘favoritePrimes’ Error: Type ‘FavoritePrimesAction’ has no member ‘counter’ For the first, the fix is easy: we can un-nest the action: self.store.send(.deleteFavoritePrimes(indexSet))

10:22

The second error is a good one to have! The favorite primes view was sending a counter action when it really shouldn’t have, so we can delete that line of code and appreciate how focused this view has become.

10:40

We still have one thing to fix. Where we initialize FavoritePrimesView : NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view( value: { $0.favoritePrimes }, action: { $0 } ) ) ) ‘Store<[Int], AppAction>’ is not convertible to ‘Store<[Int], FavoritePrimesAction>’

10:50

We need to take our store that works with app actions and produce a view of that store that works with favorite primes actions. We can do so by taking the FavoritePrimesAction passed to the action closure, and embedding it in the favoritePrimes case of AppAction : NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view( value: { $0.favoritePrimes }, action: { AppAction.favoritePrimes($0) } ) ) )

11:02

We can even use type inference to simplify things: NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view( value: { $0.favoritePrimes }, action: { .favoritePrimes($0) } ) ) )

11:10

There’s even a nice symmetry now. Extracting our first modular view

11:27

Before we update all the other views, let’s stop and appreciate what we’ve done: FavoritePrimesView is now as focused as it can be. It has no knowledge of AppState : it only has access to the [Int] of favorite primes that it renders. And it has no way of sending AppAction s: it can only send FavoritePrimesAction s. This is the kind of modularity we’ve been striving for. And it means that we can finally extract this view to its own module.

11:52

While it could live in a module of its very own, let’s move it to the “FavoritePrimes” module for now. We can cut and paste it: struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], FavoritePrimesAction> var body: some View { … } } Use of undeclared type ‘View’

12:04

But it depends on a bunch of other modules, including Combine, SwiftUI, and even our “ComposableArchitecture” framework, so let’s import them. import ComposableArchitecture import SwiftUI

12:20

Things compile, so they are isolated, but we need to make things public, namely: the struct itself and its body. public struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], FavoritePrimesAction> public var body: some View { … } }

12:39

If we try to build our application, we get an error. ‘FavoritePrimesView’ initializer is inaccessible due to ‘internal’ protection level

12:45

In moving the view to a module we encounter a problem similar to when we moved prime modal state into a module: the member-wise initializer that Swift automatically generates for us is internal, but we need a public initializer to produce a value in our app.

12:57

So we need to define it from scratch: public struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], FavoritePrimesAction> public init(store: Store<[Int], FavoritePrimesAction>) self.store = store } public var body: some View { … } }

13:07

And with that, everything builds and we have truly, in a compiler-checked way, isolated this view from the rest of our application. Doing so came with a bit more boilerplate in the form of an explicit initializer, but like the boilerplate we’ve adopted earlier in the series, it’s completely mechanical to write, and future versions of Xcode will be able to write it for us. And it’s completely worth it to guarantee that our view has a very limited ability to access more global actions and state. Focusing on prime modal actions

13:43

Alright, we have a few more views we can isolate from AppAction , like IsPrimeModalView : struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, AppAction>

13:52

It only needs to send PrimeModalAction s. struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, PrimeModalAction>

13:59

Which simplifies how actions are called by eliminating some more nesting: Button("Remove from favorite primes") { self.store.send(.removeFavoritePrimeTapped) } … Button("Save to favorite primes") { self.store.send(.saveFavoritePrimeTapped) }

14:06

And where the view is initialized, we have to provide a store that limits its actions. IsPrimeModalView( store: self.store.view( value: { ($0.count, $0.favoritePrimes) } action: { .primeModal($0) } ) ) The more local action needs to be embedded in an AppAction .

14:25

And now we have another view that has been completely isolated.

14:32

Let’s cut and paste the view and its isPrime helper into the “PrimeModal” module. struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, PrimeModalAction> var body: some View { … } } func isPrime(_ p: Int) -> Bool { … }

14:46

Again, we have a few imports to add. import ComposableArchitecture import SwiftUI

14:58

We have to publicize the view’s interface. public struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, PrimeModalAction> public var body: some View { … } }

15:05

And we have to define a public initializer. public init(store: Store<PrimeModalState, PrimeModalAction>) { self.store = store }

15:13

It was the exact same process as last time. And when we build our app, everything still compiles just fine, and the whole process only took a matter of minutes. Focusing on counter actions

15:24

We’ve now extracted two views from our application into their own modules and limited their surface area dramatically. They no longer have access to all of app state and app actions.

15:47

We have one more view, but it’s a bit more complicated than the others, so let’s try to tackle it and see what happens.

16:00

If we take a look at our counter view, it’s big one. struct CounterView: View { @ObservedObject var store: Store<CounterViewState, AppAction> And it still works with all of app actions.

16:17

It needs access to CounterAction s for the counter that it displays, and it needs access to PrimeModalAction s in order to hand them off to the IsPrimeModalView . It does not need access to FavoritePrimesAction s and shouldn’t have the ability to send them to the store. But how do we limit the view from sending those actions while retaining the ability to send either counter actions or prime modal actions?

16:44

We can do so in a way similar to the way we limited the state that the prime modal had access to: by creating an intermediate type. But this time, instead of a struct, we want to define an enum that has a case per action it handles. enum CounterViewAction { case counter(CounterAction) case primeModal(PrimeModalAction) }

17:40

And with this defined, we can swap out the store’s action type: struct IsPrimeModalView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction>

17:39

Nothing else needed to change. Because the case names match up, we were able to make this refactor and leave the view’s body entirely intact.

17:53

Things aren’t quite building yet, though, because the CounterView is being initialized with the wrong store type. NavigationLink( "Counter demo", destination: CounterView( store: self.store.view( value: { ($0.count, $0.favoritePrimes) }, action: { $0 } ) ) ) Cannot convert value of type ‘CounterViewAction’ to closure result type AppAction

17:55

The action block is now passed a CounterViewAction , and we need to return an AppAction instead. There is no AppAction case that embeds a CounterViewAction , but instead, we can switch over the container action to unwrap each case and rewrap them in the appropriate AppAction . NavigationLink( "Counter demo", destination: CounterView( store: self.store.view( value: { ($0.count, $0.favoritePrimes) }, action: { switch $0 { case let .counter(action): return AppAction.counter(action) case let .primeModal(action): return AppAction.primeModal(action) } } ) ) )

18:39

This is enough to satisfy the compiler, but it’s a lot to write inline, especially when we’ve been aiming to simplify views. Luckily, it’s very simple logic and we’ll further streamline it in a moment.

18:51

Alright, the counter view is fully ready to be moved into the “Counter” module. We can cut and paste the pieces we need, including the view, the PrimeAlert wrapper struct, the intermediate CounterViewAction , and a couple helpers. struct PrimeAlert: Identifiable { … } enum CounterViewAction { … } struct CounterView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction> … var body: some View { … } } func nthPrime(_ n: Int, callback: @escaping (Int?) -> Void) -> Void { … } func ordinal(_ n: Int) -> String { … }

19:30

Let’s add those imports. import ComposableArchitecture import SwiftUI

19:37

We have a compiler error because we rely on PrimeModalAction , which lives in its own module. enum CounterViewAction { case counter(CounterAction) case primeModal(PrimeModalAction) Use of undeclared type ‘PrimeModalAction’

19:53

So we need to update the framework to depend on this module before importing it, as well. import PrimeModal

20:21

But we still have one error: Use of unresolved identifier ‘wolframAlpha’ Because we also need to move “WolframAlpha.swift” into the module because the nthPrime helper uses it.

20:43

Alright, the framework seems to compile, so let’s audit the public interface. The intermediate counter container action needs to be public so that our app can create a store that is focused on these actions. public typealias CounterViewState = ( count: Int, favoritePrimes: [Int] ) public enum CounterViewAction { … }

21:02

And finally, once again the view needs to be public and given an explicit, public initializer. public struct CounterView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction> … var body: some View { … } }

21:14

And none of the internal helpers need to be made public. They could even be made private!

21:21

Everything still builds, but the counter view has been fully extracted into its own module. We can even run the application to make sure everything works, just as before, even though each screen lives in its own module. Next time: what’s the point?

22:11

Alright, we’ve now accomplished what we set out to do at the beginning of this series of episodes on modular state management. First, we defined what “modular” meant: we determined that modularity was the ability to move code into a literal Swift module so that it is completely isolated from the rest of your app. Then we showed just how modular our reducers already were by breaking each one off into a framework of its own. Finally, in order to modularize our views we discovered two forms of composition on the Store type: one that allowed us to focus an existing store on some subset of state, and another that allowed us to focus an existing store on some subset of actions. These operations allowed us to separate all of our views from more global, app-level concerns and move them into isolated modules of their own.

23:30

But whenever we conclude one of these explorations on Point-Free, we like to stop, reflect, and ask: “What’s the point!?”

23:42

In order to meet our requirements for modularity, we had to: Create a new framework in Xcode for every component. This can be a cumbersome thing to do, and can lead to complicated project build settings. Identify and move the code that should move to the module, and audit the public interface that the module needs to expose. Add boilerplate around public initializers, and sometimes even introduce intermediate structs for state and intermediate enums for actions.

24:14

Did we even really need this level of modularity? Why couldn’t we have moved things into separate files and introduce private and fileprivate access instead? Why not avoid the extra boilerplate if we can? It’s quite a bit of extra work. Is it actually worth doing?

24:35

We’ve said it before throughout the entire series, but we’ll say it again: we absolutely believe that this is worth doing.

24:42

Each module is now hyper-focused on a small slice of app state, logic, and views, enforced by the compiler. Your teammates and future self can now read these modules and understand them at-a-glance in ways that would have otherwise required a lot more time and effort.

25:13

It is true that you can organize code into more isolated files, and that introducing private and fileprivate access can limit the amount of code that leaks out. However, it does nothing to prevent these files from having access to everything else in the module. And in a large app target, that can be a lot.

25:29

The cost is a little bit of up-front work creating the module, moving the code, and writing a limited amount of boilerplate. But the benefit is in all the work saved later on.

25:35

We can also put this modularity to the test by showing how these screens can be run in isolation. For example, we can load up any of these screens in a playground and experiment with it right there and work with them as if they are applications of their own…next time! References Contravariance Brandon Williams & Stephen Celis • Apr 30, 2018 We first explored the concept of the pullback in our episode on “contravariance”, although back then we used a different name for the operation. The pullback is an instrumental form of composition that arises in certain situations, and can often be counter-intuitive at first sight. Note Let’s explore a type of composition that defies our intuitions. It appears to go in the opposite direction than we are used to. We’ll show that this composition is completely natural, hiding right in plain sight, and in fact related to the Liskov Substitution Principle. https://www.pointfree.co/episodes/ep14-contravariance Why Functional Programming Matters John Hughes • Apr 1, 1989 A classic paper exploring what makes functional programming special. It focuses on two positive aspects that set it apart from the rest: laziness and modularity. https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf Access Control Apple This chapter of the Swift Programming Language book explains access control in depth and how it affects module imports. https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html Elm: A delightful language for reliable webapps Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state. https://elm-lang.org Redux: A predictable state container for JavaScript apps. The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm . https://redux.js.org Composable Reducers Brandon Williams • Oct 10, 2017 A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”. https://www.youtube.com/watch?v=QOIigosUNGU Downloads Sample code 0074-modular-state-management-view-actions 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 .