EP 75 · Modular State Management · Oct 7, 2019 ·Members

Video #75: Modular State Management: The Point

smart_display

Loading stream…

Video #75: Modular State Management: The Point

Episode: Video #75 Date: Oct 7, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep75-modular-state-management-the-point

Episode thumbnail

Description

We’ve now fully modularized our app by extracting its reducers and views into their own modules. Each screen of our app can be run as a little app on its own so that we can test its functionality, all without needing to know how it’s plugged into the app as a whole. And this is the point of modular state management!

Video

Cloudflare Stream video ID: c4e98b7197a6b261f425a55d57c35333 Local file: video_75_modular-state-management-the-point.mp4 *(download with --video 75)*

References

Transcript

0:05

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. What’s the point?

1:24

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

1:36

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.

2:08

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?

2:29

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

2:36

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.

3:07

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.

3:23

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.

3:29

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.

3:47

Let’s hop over to our playground to get started. The favorite primes app

4:01

The FavoritesPrimeView is probably the simplest to start with, all we have to do is creating a hosting view controller with our view configured with a store: import ComposableArchitecture import FavoritePrimes import SwiftUI import PlaygroundSupport PlaygroundPage.current.liveView = UIHostingController( rootView: FavoritePrimesView( store: Store<[Int], FavoritePrimesAction>( initialValue: [], reducer: favoritePrimesReducer ) ) )

5:18

The view consists of just an empty this, but that’s because we didn’t give it any data for its initial value.

5:22

If we provide some data we will see the list popular and we can remove from the list: PlaygroundPage.current.liveView = UIHostingController( rootView: FavoritePrimesView( store: Store<[Int], FavoritePrimesAction>( initialValue: [2, 3, 5, 7, 11], reducer: favoritePrimesReducer ) ) )

5:36

So this view can be run in isolation, and uses a store to persist its mutations locally right here in the playground. Yet when it’s run in the fully application, the store it is handed is actually just a mere view into the global store that powers the whole app. In fact, the playground doesn’t and can’t have access to the main application code. The prime modal app

6:24

Okay, the favorite primes view was pretty easy, let’s try something a little more complicated: the prime modal view. Can we run it off a store in complete isolation? import PrimeModal PlaygroundPage.current.liveView = UIHostingController( rootView: IsPrimeModalView( store: Store<PrimeModalState, PrimeModalAction>( initialValue: (0, []), reducer: primeModalReducer ) ) )

7:05

This immediately shows us what the view looks like when we are showing a non-prime. And if we update our initial value we will see what it looks like at a prime: initialValue: (2, []),

7:12

We can even tap the add/remove button a few times and it is actually mutating this array of favorites.

7:20

We can also start with some favorites in the initial value so that we can make sure that looks right: initialValue: (2, [2, 3, 5]),

7:34

Here’s one more view running off a store created right in the playground. It’s not being run off our application’s global store. The counter app

7:45

So these two screens has been amazingly easy to load up and run in isolation. Let’s try our last screen, the counter view. If we try to do what we did for the previous screens, we’re going to run into a problem: import Counter PlaygroundPage.current.liveView = UIHostingController( rootView: CounterView( store: Store<CounterViewState, CounterViewAction>( initialValue: (0, []), reducer: <#???#> ) ) )

8:06

We don’t have a counter view reducer to plug in here. We have a counter reducer, but it’s just focused on the incrementing and decrementing logic. We also have a prime modal reducer, which is focused on showing the prime modal and managing adding and removing a prime from one’s favorites. We want a reducer that bundles these two reducers up.

8:25

It’s starting to seem like maybe our reducers aren’t factorized in the right way. The problem here is that we don’t have a single reducer that represents the counter view, but rather we have two reducers that get combined in the app reducer: pullback(counterReducer, value: \.count, action: \.counter), pullback( primeModalReducer, value: \.primeModal, action: \.primeModal ), This combination of reducers is what actually powers the counter view, yet it lives in our app target and not the Counter model. And even worse, everything in the app target is inaccessible in playgrounds.

8:51

Perhaps this composed reducer should be pulled out into a counterViewReducer that operates on CounterView . Let’s copy these lines, comment them out, and then switch over to the Counter module (actually switch target to Counter too) to set up a basic reducer signature: let counterViewReducer: (inout CounterViewState, CounterViewAction) -> Void = combine( pullback(counterReducer, value: \.count, action: \.counter), pullback(primeModalReducer, value: \.primeModal, action: \.primeModal), )

9:19

This should mostly work, but one problem here is that we no longer have the enum key paths for pulling back. We want to be able to pullback counter and prime modal actions into the CounterViewAction enum. Those key paths currently live on our AppAction , which is exactly what we want to factor out into our Counter module.

9:37

Let’s just cut and paste those right into our CounterViewAction enum: var counter: CounterAction? { get { guard case let .counter(value) = self else { return nil } return value } set { guard case .counter = self, let newValue = newValue else { return } self = .counter(newValue) } } var primeModal: PrimeModalAction? { get { guard case let .primeModal(value) = self else { return nil } return value } set { guard case .primeModal = self, let newValue = newValue else { return } self = .primeModal(newValue) } }

9:59

Now our combined reducer is nearly compiling, but we no longer need to pullback prime modal state because it now operates on the full counter view state. Both features use the counter and favorite primes. So we can use the identity key path to fix this compiler error: public let counterViewReducer: (inout CounterViewState, CounterViewAction) -> Void = combine( pullback(counterReducer, value: \.count, action: \.counter), pullback(primeModalReducer, value: \.self, action: \.primeModal) )

10:31

And we can even remove the explicit reducer signature if we just put in some key path types, which is maybe a little less noisy: public let counterViewReducer = combine( pullback( counterReducer, value: \CounterViewState.count, action: \CounterViewAction.counter ), pullback(primeModalReducer, value: \.self, action: \.primeModal) )

10:56

Our Counter module is now building, and we can even switch back to our playground, update the code to use this new reducer, and run this screen in full isolation: import Counter PlaygroundPage.current.liveView = UIHostingController( rootView: CounterView( store: Store<CounterViewState, CounterViewAction>( initialValue: (2, []), reducer: counterViewReducer ) ) )

11:24

But now we can easily test out states that would have been difficult to recreate otherwise. For instance, we can simulate having counted up to a million. initialValue: (1_000_000, []),

11:39

And now we can ask Wolfram Alpha for the millionth prime.

11:50

This is pretty amazing! These 3 screens can be run in full isolation, yet they can all be pieced together so that they are all driven off of the same global state. This is what we mean by a modular, composable architecture!

12:05

Soon we will even be able to remove this playground and instead use an Xcode preview, which allows you to run your view directly in Xcode. This will be very powerful, but unfortunately the computer we are recording on doesn’t have Catalina installed to demonstrate this, and the previews feature isn’t quite stable enough for us to use in an episode.

12:26

There is something magical about what is happening here. Right now our store really has very little to do with SwiftUI directly. In fact, we will have future episodes where we show how our store can be adapted to be used in a UIKit application, and even with legacy Objective-C code! So this architecture goes far beyond just SwiftUI, though it certainly shines when used with SwiftUI, which means that we could run even UIView s and UIViewController s in isolation like this. You will be able to just create a store with the appropriate initial value and reducer, hand it off to the controller, and away you go! This takes “ playground driven development ” to a whole new level. Fixing the root app

13:25

But, as amazing all of this is, we have actually broken our main app target with our refactor, so let’s quickly fix that.

13:49

What we want to do is compose our app reducer out of the favorite primes reducer and our brand new counter view reducer. let appReducer: (inout AppState, AppAction) -> Void = combine( // pullback(counterReducer, value: \.count, action: \.counter), // pullback( // primeModalReducer, value: \.primeModal, action: \.primeModal // ), pullback( counterViewReducer, value: \.counterView, action: \.counterView ), pullback( favoritePrimesReducer, value: \.favoritePrimes, action: \.favoritePrimes ) )

14:16

Unfortunately, these key paths into counterView do not exist right now. So let’s fix that.

14:27

To begin with, let’s refactor our AppAction , because we now want to bundle all of the counter view actions into just one case: // case counter(CounterAction) // case primeModal(PrimeModalAction) case counterView(CounterViewAction)

14:33

We also want to generate a new enum property for the counterView case. Every time we embed a more local action in a global action, we need an enum key path that will allow us to pull back along that view in order to compose reducers together. Enum properties give us these key paths, and we have spent several episodes in the past writing a tool that will generate these properties for us automatically.

15:01

So let’s hop on over to the command line and run it. $ swift run generate-enum-properties ./PrimeTime/ContentView.swift Updating PrimeTime/ContentView.swift

15:20

The tool has now automatically inserted a new enum property for the counterView case. var counterView: CounterViewAction? { get { guard case let .counterView(value) = self else { return nil } return value } set { guard case .counterView = self, let newValue = newValue else { return } self = .counterView(newValue) } }

15:27

This has satisfied half of the pullback on the counter view reducer. pullback( counterViewReducer, value: \.counterView, action: \.counterView ),

15:36

For counter view state, getting a writable key path is pretty easy, we already have a getter we used previously pulling back on prime modal state, and upgrade it to work on counter view state: extension AppState { var counterView: CounterViewState { get { CounterViewState( count: self.count, favoritePrimes: self.favoritePrimes ) } set { self.count = newValue.count self.favoritePrimes = newValue.favoritePrimes } } } We can update it in place because we don’t even need the application target to worry about prime modal state. All of that concern has been pushed down into the counter module.

16:05

The app reducer is now successfully being composed from the counter view reducer and the favorite primes reducer, but we’re not quite building yet, because the activity feed is still working in a flat world of counter and prime modal actions. We can fix this easily by nesting each of these cases further. case .counterView(.counter): … case .counterView(.primeModal(.removeFavoritePrimeTapped)): … case .counterView(.primeModal(.saveFavoritePrimeTapped)): …

16:33

We have one final set of errors to deal with, where we are transforming our global store into a counter view store. 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) } } ) ) ) Type ‘AppAction’ has no member ‘counter’ Type ‘AppAction’ has no member ‘primeModal’

16:41

Here we were switching over each counter view case manually in order to re-embed it in the appropriate app action. But now that AppAction has a specific counterView case, we can simplify this to a one-liner. NavigationLink( "Counter demo", destination: CounterView( store: self.store .view( value: { ($0.count, $0.favoritePrimes) }, action: { .counterView($0) } } ) ) )

16:57

We also have that counterView property on AppState , so if we want things to look nice and symmetrical, we can use that instead of manually building our tuple. NavigationLink( "Counter demo", destination: CounterView( store: self.store .view( value: { $0.counterView }, action: { .counterView($0) } } ) ) )

17:14

It’s worth noting just how simple the main app has gotten! All it’s responsible for is declaring full app state, app actions, and then composing everything else together from the modules it imports into an app reducer and a root content view. It’s even shrunk 200 lines from the start of this series on modularity!

17:50

And in our most recent refactor, we pushed everything about the prime modal module into the counter view module, so we can even delete an additional line, where we import the prime modal module, which guarantees that the main application knows nothing directly about the prime modal screen. // import PrimeModal Conclusion

18:07

We’ve done what we sought out to do: we’ve fully modularized our application by moving each screen’s state, actions, reducer, and view into their own modules. And we’ve modularized it in such a way that we can run each screen in its own playground, completely isolated from the rest of the main application.

18:25

So, this is the point of investing even more energy into this reducer architecture. By doing a dedicated study of how how reducers and stores can be decomposed and composed, we have given ourselves the tools to break an application down into many smaller applications. Each of these applications can even live in their own Swift module, which means it is completely impossible for one to interact with the other unless there is an explicit dependency of one on the other.

18:45

We could take this even further by testing these modules: not only unit testing, but snapshot testing! You would be able to create a store with any initial state you want, pass it off to a view, and then snapshot the controller. You could even go further by sending actions to the store to mimic what would happen if the user were tapping around on the screen, and then take another snapshot of the controller.

19:31

Okay, we’ve now seen that we can fully modularize our reducers and their views. We’ve fully solved 3 of the 5 problems we identified that tend to encumber application architecture. That means we still have 2 more to solve:

19:41

We have yet to introduce a story for side effects.

19:45

And we have yet to prove how testable our architecture is.

19:50

These are two important problems that will be fun to solve. Till next time! References Playground Driven Development Brandon Williams & Stephen Celis • Jul 9, 2018 This week’s episode took “playground-driven development” to the next level by showing that a fully modularized app allows each of its screens to be run in isolation like a mini-app on its own. Previously we talked about playground-driven development for quickly iterating on screen designs, and showed what is necessary to embrace this style of development. Note We use Swift playgrounds on this series as a tool to dive deep into functional programming concepts, but they can be so much more. Today we demonstrate a few tricks to allow you to use playgrounds for everyday development, allowing for a faster iteration cycle. https://www.pointfree.co/episodes/ep21-playground-driven-development Playground Driven Development Brandon Williams • Oct 6, 2017 Brandon gave an in-depth talk on playground driven development at FrenchKit 2017. In this talk he shows what it takes to get a codebase into shape for this style of development, and shows off some of the amazing things you can do once you have it. https://www.youtube.com/watch?v=DrdxSNG-_DE Playground Driven Development at Kickstarter Brandon Williams • May 19, 2017 We pioneered playground driven development while we were at Kickstarter, where we replaced the majority of our use for storyboards with playgrounds. It takes a little bit of work to get started, but once you do it really pays dividends. In this Swift Talk episode, Brandon sits down with Chris Eidhof to show the ins and outs of playground driven development. https://talk.objc.io/episodes/S01E51-playground-driven-development-at-kickstarter 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 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 0075-modular-state-management-wtp 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 .