EP 95 · Adaptive State Management · Mar 23, 2020 ·Members

Video #95: Adaptive State Management: State

smart_display

Loading stream…

Video #95: Adaptive State Management: State

Episode: Video #95 Date: Mar 23, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep95-adaptive-state-management-state

Episode thumbnail

Description

There’s a potential performance problem lurking in the Composable Architecture, and it’s time to finally solve it. But, in doing so, we will stumble upon a wonderful way to make the architecture adaptive to many more situations.

Video

Cloudflare Stream video ID: 8bdd6c4f0f79a72da1792631eba12884 Local file: video_95_adaptive-state-management-state.mp4 *(download with --video 95)*

Transcript

0:05

Any action we take is causing the UI to freeze up, ostensibly because SwiftUI is doing a ton of work to figure out how to render the root ContentView . So, it definitely is possible for our design choices for the store to cause some performance problems in SwiftUI, but it’s not quite clear how big of a problem this is in a real world application.

0:26

So we’ve now seen that there is a potential problem with the way we have set up the Composable Architecture. Again, we say “potential” because it may not actually become a problem until you have a massive application. But, rather than waiting for that day to come, it turns out there is something pretty simple we can do to fix this. Even better, by fixing this problem we will actually stumble upon a wonderful way to make all of our business logic in our reducers more adaptable to platforms and the constraints we face in real world applications.

0:55

The crux of the problem is that we have a single store inside our views that holds not only all the state it needs to show its UI, but also all the state of its children, which may not be shown until later, if at all. So it seems like perhaps our views need to hold onto a different object that only has access to the state that the view cares about, and nothing more. And then maybe we have a better chance of getting notified of only the changes to the state that we actually care about. And if we succeed in doing that, then maybe the Store doesn’t need to be an observable object at all, only this secondary thing needs to be observable.

1:33

Before figuring out how to do that, let’s do the one thing we know needs to be done. View models and view stores

1:58

Let’s remove the ObservableObject conformance from the Store so that we stop over notifying views of state changes. We’ll also remove the public access to the value since views shouldn’t be using it to render its UI: public final class Store<Value, Action> /* : ObservableObject */ { … @Publisher private var value: Value

2:21

We’ll keep the property as @Published because we do want to replay any changes made to the value to all of the derived sub-stores, and having a publisher of values makes this very easy to do.

2:34

Now let’s figure out how to implement this secondary object which will be the thing actually responsible for notifying the view of state changes. What should we name it? We are going to take some inspiration from terms used in the iOS community for a long time, and even Apple recently started using this term in WWDC sessions on SwiftUI. The term “view model” has long be used as a way to describe the domain that precisely describes a view. It provides a nice abstraction between your business logic and your view. The business logic may have concepts that don’t directly map to UI like the loading state of inflight API requests, whereas the view has very concrete things that the user can see like the enabled or disabled state of a button.

3:28

So, using “view model” as inspiration we will call the new concept we are introducing to the architecture “view store”: ViewStore { }

3:48

We should think of this as a store that is fine-tuned for working directly with views. Where stores hold all of the messy business logic of our application, and thus may contain a lot more information than our view cares about, the ViewStore will hold only the domain that this view cares about. It doesn’t even need to hold the domain of its children views if it doesn’t need that information.

4:10

We want this object to interact with SwiftUI so that it can trigger re-renders of the view, and therefore it needs to be a class: class ViewStore { }

4:18

It also needs to be an ObservableObject in order for SwiftUI to be able to hook into its notifications: public final class ViewStore: ObservableObject { }

4:25

To be an observable object it needs to hold onto a publisher that emits anytime a value in the store is about to be mutated. SwiftUI provides an easy way to simultaneously declare storage for a value inside this class and automatically ping that publisher under the hood when the value is mutated: final class ViewStore<Value>: ObservableObject { @Published public fileprivate(set) var value: Value }

5:10

And due to how classes work in Swift we must also provide an initializer: public final class ViewStore<Value>: ObservableObject { @Published public private(set) var value: Value init(initialValue: Value) { self.value = initialValue } }

5:34

This is the basics of the ViewStore . It simply wraps some value in a class and exposes it as an observable object.

5:45

The real work to make this thing a useful concept for our architecture is to implement ways of creating ViewStore s from regular Store s. So, given a store that understands a particular domain of values and actions, we’d like to be able to derive a ViewStore from it that prevents notifying for state changes that are outside of its domain.

6:14

To start, let’s say that the way we will derive a view store from a store is via a computed property: extension Store { var view: ViewStore<Value> { <#???#> } }

6:44

Before even trying to figure out how to implement this property we should mention that we already have a view method on Store and it is what we used to pass along focused stores from parent views to child views. We named this method before we had even considered the concept of a ViewStore , and so in retrospect it seems a little confusing to have both of these names at the same time.

7:08

Let’s rename the original view method so that we can free up that name for the ViewStore transformation. But, what do we rename it to? We could call it something like focus , but that’s quite similar to view and so seems a little weird. We could also go very generic and just call it transform , but that doesn’t help convey that we are specifically transforming global stores into local stores. So, the name we like for this is scope , as it will not be confused with view and it helps us understand that there is some kind of limiting process being applied to our store: public func scope<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action ) -> Store<LocalValue, LocalAction> { … }

7:57

And now we can think of this focussing operation not as “viewing” into a store, but rather “scoping” a store to a child domain.

8:10

So let’s figure out the signature of the view method: extension Store { var view: ViewStore<Value> { <#???#> } }

8:14

Well, the way to construct a view store is via its initializer, which takes an initial value: extension Store { var view: ViewStore<Value> { ViewStore(initialValue: self.value) } }

8:28

That gets things compiling, but of course this isn’t right. As it is now the ViewStore will never have its value updated, and therefore it will never notify a SwiftUI view that it needs to re-render.

8:44

We need to do something similar to what we do in the scope method, which is to subscribe to all the changes in the store’s value and replay them to the view store. extension Store { var view: ViewStore<Value> { let viewStore = ViewStore(initialValue: self.value) self.$value.sink(receiveValue: { value in viewStore.value = value }) return viewStore } }

9:19

Now things are compiling, but we have a warning about the unused return value from sink . This method returns a cancellable, which allows us to stop receiving values from a publisher when we want. In particular, if we store a cancellable in our ViewStore : public final class ViewStore<Value>: ObservableObject { … fileprivate var cancellable: Cancellable?

9:57

And assign that value when we call sink : viewStore.cancellable = self.$value.sink(receiveValue: { value in viewStore.value = value }) Then we are guaranteed that when the ViewStore is deallocated we will stop receiving values from this publisher. View store performance

10:05

We are getting closer to a useful implementation of this method, but something still isn’t quite right. We still aren’t doing any work to make sure that the view store doesn’t over-emit values to its view. As it stands, every time the global store changes all of its associated view stores will also change, and hence trigger view re-computations.

10:42

Luckily we can use some of the machinery that Combine gives us to make sure that the view store does not emit any duplicate values. We just need to use the removeDuplicates methods: viewStore.cancellable = self.$value .removeDuplicates() .sink(receiveValue: { value in viewStore.value = value })

10:57

But this doesn’t work because our value isn’t necessarily equatable. Let’s constrain this extension to make it so: extension Store where Value: Equatable { var view: ViewStore<Value> { … } }

11:15

And now we have a real implementation of this property. This allows us to derive a ViewStore from any store, as long as our state is equatable.

11:34

Usually when one creates an API that requires some type to be equatable, a closely related, but alternative, form of that API is provided for when we can’t use an equatable type. For example, the removeDuplicates method has an overload where you get to provide the condition explicitly for when duplicates should be removed: .removeDuplicates(by: <#(Equatable, Equatable) -> Bool#>)

12:09

We’ll want to do this too because we sometimes make use of tuples for our features’ state, and unfortunately tuples cannot be equatable. So, we’ll make an overload of view that allows us to do this: extension Store { public func view( removeDuplicates predicate: @escaping (Value, Value) -> Bool ) -> ViewStore<Value> { let viewStore = ViewStore(initialValue: self.value) viewStore.cancellable = self.$value .removeDuplicates(by: predicate) .sink { newValue in viewStore.value = newValue } return viewStore } }

12:47

And even the other implementation can be written in terms of this one: extension Store where Value: Equatable { public var view: ViewStore<Value> { self.view(removeDuplicates: ==) } }

13:02

Before moving on let’s go ahead and fix a memory leak problem. Right now the cancellable is holding onto the viewStore in this closure, but also the view store itself is holding onto the cancellable as a property. This is the same problem we have when scoping stores, and the fix is to weakly hold onto the view store in the closure: .sink { [weak viewStore] newValue in viewStore?.value = newValue }

13:31

And this is our implementation of view on stores.

13:36

Using ViewStore s

13:36

This has caused compiler errors everywhere we tried accessing the value from the store. We hope that by replacing all of those uses of the store with a suitably constructed ViewStore we will prevent needless re-computations and renders of our views.

14:01

Let’s start at the simplest of our features and work backwards. We want to mention something that we brought up many times during our episodes on the Composable Architecture, which is that due to our efforts in modularizing the application we get the opportunity to incrementally fix these compiler errors. We can just concentrate on one module and get it building in isolation, as opposed to trying to get the entire application fixed all at once. That makes it much easier for us to experiment with these kinds of changes to our architecture and libraries. That’s only possible due to our ability to modularize, and the Composable Architecture made it very easy for us to modularize.

14:46

In the FavoritePrimes package we have the following error: Property type ‘Store<[Int], FavoritePrimesAction>’ does not match that of the ‘wrappedValue’ property of its wrapper type ‘ObservedObject’

14:55

This is because stores are no longer observable. We instead need a view store. We can declare a new field on our view: public struct FavoritePrimesView: View { let store: Store<FavoritePrimesState, FavoritePrimesAction> @ObservedObject var viewStore: ViewStore<FavoritePrimesState>

15:23

Right now the store and view store work with the same state, but that’s just coincidental for this view. It doesn’t need to be that way, and often it should not.

15:31

In the body of the view instead of accessing the current value from the store, we now need to go through the view store: ForEach(self.viewStore.value.favoritePrimes, id: \.self) { prime in Text("\(prime)") } … .alert(item: .constant(self.viewStore.value.alertNthPrime)) {

15:44

And our final error in this file has to do with the fact that we haven’t created the view store: Return from initializer without initializing all stored properties To create the view store we just need to view into the store to derive its view store: public init( store: Store<FavoritePrimesState, FavoritePrimesAction> ) { print("FavoritePrimesView.init") self.store = store self.viewStore = store.view }

16:07

This doesn’t work because FavoritePrimesState is a tuple, and tuples in Swift are not equatable. That may be fixed someday, but until then there is an ad hoc solution. The Swift standard library defines an overload of the == operator for a few sizes of tuples, I believe up to 6 components. And therefore we can use our overload of view that takes an explicit equality function: self.viewStore = self.store.view(removeDuplicates: ==)

16:33

There have been Swift evolution proposals to allow tuples to become equatable when all of their components are, and so hopefully we will not have to do this someday soon.

16:41

This gets things compiling, and now we can rest assured that this view will re-compute itself only if its local state changes, in particular the array of favorite primes and alert. Any other changes to the global app state should have no effect on this view.

16:57

Next, let’s go to the PrimeModal package. It has a similar error to what we’ve already seen: Property type ‘Store<PrimeModalState, PrimeModalAction>’ (aka ‘Store<(count: Int, favoritePrimes: Array ), PrimeModalAction>’) does not match that of the ‘wrappedValue’ property of its wrapper type ‘ObservedObject’

17:06

This is because Store is no longer observable, so we need to use a ViewStore : public struct IsPrimeModalView: View { let store: Store<PrimeModalState, PrimeModalAction> @ObservedObject var viewStore: ViewStore<PrimeModalState>

17:25

Next we need to assign the viewStore property in the initializer: public init(store: Store<PrimeModalState, PrimeModalAction>) { print("IsPrimeModalView.init") self.store = store self.viewStore = self.store.view(removeDuplicates: ==) }

17:38

And then instead of accessing state from the store we must instead use the view store: public var body: some View { print("IsPrimeModalView.body") return VStack { if isPrime(self.viewStore.count) { Text("\(self.viewStore.value.count) is prime 🎉") if self.viewStore.favoritePrimes.contains(self.viewStore.count) { Button("Remove from favorite primes") { self.store.send(.removeFavoritePrimeTapped) } } else { Button("Save to favorite primes") { self.store.send(.saveFavoritePrimeTapped) } } } else { Text("\(self.viewStore.value.count) is not prime :(") } } }

17:49

And now our PrimeModal package is building.

17:59

Switching to the Counter package we see all the same compiler errors. We need to get rid of the observable store and instead use a view store. public struct CounterView: View { let store: Store<CounterViewState, CounterViewAction> @ObservedObject var viewStore: ViewStore<CounterViewState>

18:19

And we need to construct the view store for this view: public init(store: Store<CounterViewState, CounterViewAction>) { print("CounterView.init") self.store = store self.viewStore = self.store.view }

18:25

This time we do not need to use removeDuplicates , because CounterViewState conforms to Equatable .

18:31

And then finally there are 6 places where we need to access the view’s state through the view store instead of the store. Counter view performance

18:41

But we can improve this. Right now our view doesn’t need access to the entirety of CounterViewState , it only uses a subset of it. To see this, let’s hop over to our Counter playground and see what gets printed to the console as we click around.

19:02

We will see that we get new body computations every time we change the count, but also if we open the prime modal and save and remove from the favorites list we get more body computations: IsPrimeModalView.body CounterView.body IsPrimeModalView.init IsPrimeModalView.body

19:16

This happens even though the the counter view doesn’t actually display any favorite primes.

19:20

In order to whittle the state of this view down to its bare essentials, the minimum the view needs to do its job, let’s put an empty type in the view store and then slowly add back properties it needs: public struct CounterView: View { struct State: Equatable { } … @ObservedObject var viewStore: ViewStore<State>

19:49

This instantly gives us a compiler error for each time we try to access some state inside the store. For instance, we can see we need access to the isNthPrimeButtonDisabled boolean, so let’s add it to the store’s value type: struct State: Equatable { let isNthPrimeButtonDisabled: Bool }

20:02

Next we see it needs the isPrimeModalShown boolean: struct State: Equatable { let isNthPrimeButtonDisabled: Bool let isPrimeModalShown: Bool }

20:08

Then we see we need access to alertNthPrime : struct State: Equatable { let alertNthPrime: PrimeAlert? let isNthPrimeButtonDisabled: Bool let isPrimeModalShown: Bool }

20:15

And then we see we need count : struct State: Equatable { let alertNthPrime: PrimeAlert? let count: Int let isNthPrimeButtonDisabled: Bool let isPrimeModalShown: Bool }

20:23

And that fixes the compiler errors in the body of the view. So we see that we only need access to four fields: alertNthPrime , count , isNthPrimeButtonDisabled and isPrimeModalShown . Notably, we do not need access to the favoritePrimes array, which is inside the CounterViewState type. This means that any changes made to that field of our application state will not trigger our view to re-compute itself.

20:46

In order to get the full view compiling we need to construct the view store that powers this view, and we do that by first scoping the store to the bare essentials of state it cares about, and then turning it into a view store: self.viewStore = self.store .scope( value: { counterViewState in State( alertNthPrime: counterViewState.alertNthPrime, count: counterViewState.count, isNthPrimeButtonDisabled: counterViewState .isNthPrimeButtonDisabled, isPrimeModalShown: counterViewState.isPrimeModalShown ) }, action: { $0 } ) .view

21:45

You may not like having all of this logic in the initializer, and so one easy thing we can do is make a little convenience initializer at the bottom of the file: extension CounterView.State { init(counterViewState: CounterViewState) { self.alertNthPrime = counterViewState.alertNthPrime self.count = counterViewState.count self.isNthPrimeButtonDisabled = counterViewState .isNthPrimeButtonDisabled self.isPrimeModalShown = counterViewState.isPrimeModalShown } }

22:15

Ha, but this is revealing a bit of awkwardness with naming. We now have both a CounterViewState and a CounterView.State : they have the same name but contain different state. While the new State struct that is nested inside CounterView describes all of the state the counter view needs to observe, CounterViewState contains all of the state necessary to run the entire counter feature, which includes the state for the prime modal.

22:47

So maybe CounterViewState wasn’t quite the right name in the first place. We defined it to describe the union of CounterState and PrimeModalState , which is all of the state needed to run the counter feature. So maybe CounterFeatureState is more appropriate: public struct CounterFeatureState: Equatable {

23:03

And we can update the CounterView.State initializer argument: extension CounterView.State { init(counterFeatureState: CounterViewState) { self.alertNthPrime = counterFeatureState.alertNthPrime self.count = counterFeatureState.count self.isNthPrimeButtonDisabled = counterFeatureState .isNthPrimeButtonDisabled self.isPrimeModalShown = counterFeatureState.isPrimeModalShown

23:08

With that renaming we can now do: self.viewStore = self.store .scope(value: State.init(counterFeatureState:), action: { $0 }) .view

23:32

While we’re renaming things, we can also rename CounterViewAction : public enum CounterFeatureAction: Equatable { View store memory management

23:41

And now that the counter module is building again, but we have accidentally introduced a problem. If we hop over to the Counter playground, run it, and start clicking around, we will find something a little troubling. Clicking on the increment button isn’t changing the UI at all, but from the logs we can definitely see that the business logic is running and the app state is being mutated. So what is going on? If we add some logging to the reducer, we’ll see that actions are definitely being sent to the store and updating state, but changes aren’t being reflected in the UI.

24:25

Well, the thing that controls the rendering of our UI is the view store, since it’s the thing that is observable, and we just changed the construction of the view store so that it first scopes the store and then views it: self.viewStore = self.store .scope(value: State.init(counterFeatureState:), action: { $0 }) .view

24:39

In this chain of transformations we create an intermediate store when we call .scope . It turns out that nothing is actually holding onto that intermediate store, and so it is getting deallocated immediately, and therefore the view store is never notified of any changes.

24:52

To convince ourselves of this we can first see that the scoped stores definitely hold onto their parent stores by looking at this line in the implementation of scope : let localStore = Store<LocalValue, LocalAction>( initialValue: toLocalValue(self.value), reducer: { localValue, localAction, _ in self.send(toGlobalAction(localAction)) localValue = toLocalValue(self.value) return [] }, environment: self.environment )

24:58

Here we are making sure that the locally scoped store being derived holds onto the parent by referencing self inside the reducer closure. This is a good thing. It means if we call the .scope transformation multiple times on the same store we will get a chain of stores that hold onto each other.

25:19

However, the same isn’t happening when deriving the view store: public func view( removeDuplicates predicate: @escaping (Value, Value) -> Bool ) -> ViewStore<Value> { let viewStore = ViewStore(initialValue: self.value) viewStore.cancellable = self.$value .removeDuplicates(by: predicate) .sink { newValue in viewStore.value = newValue } return viewStore }

25:32

We need the derived view store to hold onto the store that it was derived from. We are soon going to have the proper way to express this relationship between store and view store, but for right now, just to get things working, we can do something a little hacky by literally referencing self inside the sink closure: viewStore.cancellable = self.$value .removeDuplicates(by: predicate) .sink { newValue in viewStore.value = newValue self }

25:49

And with that one little change our playground is now working, which means we can increment, decrement, ask if the count is prime, and save it to our favorite primes.

26:02

There is only one thing left to fix. In the main PrimeTime application target we are using a store as an observable object, and that’s no longer the right thing to do. struct ContentView: View { let store: Store<AppState, AppAction>

26:21

In all of our other views, we introduced an observable view store instead. But what state should the view store hold onto? struct ContentView: View { let store: Store<AppState, AppAction> @ObservedObject var viewStore: ViewStore<<#???#>>

26:32

If we look in the body of this view, we see that it doesn’t actually need any state to do its job. It’s just a static view hierarchy. This means we don’t even need a view store at all, we can just delete that and let the view be static: struct ContentView: View { let store: Store<AppState, AppAction> // @ObservedObject var viewStore: ViewStore<<#???#>>

26:51

The application builds, and there is no slow down once we start using the app. We can use the counter, we can ask if numbers are prime, we can save primes, and we can ask for the “nth prime”, and never once did the interface hang. If we check out the logs we see that the body property of a view is called only if its view state is directly changed. So adding a favorite prime doesn’t cause the counter view to re-render, and it certainly doesn’t cause the root content view to re-render. In fact, nothing causes the root content view to re-render once it has rendered the first time.

27:26

We have now solved the problem we set out to solve. By constructing a view store for each of our views, we can make sure that they observe only the bare essentials of the full application state changing, and therefore minimize the number of view re-computations and re-renders that happen. For the pathological example of having 500,000 rows in a single table view we were able to see a clear performance win by minimizing the amount of work our view needed to do. Adapting view stores

27:51

Since we’ve solved the performance problem we could call it a day and rest on our laurels, but this is Point-Free and we like to go deeper. There is more to view stores than meets the eye. It is not simply a means to improve performance or prevent accidental re-renders of our view. It can do a lot more than just that.

28:18

For starters, it gives us the perfect spot to move some of the logic that is in our views. For example, in the PrimeModal module we have the following line of code for determining if we want to show a save button or a remove button for the favorite primes: if self.viewStore.value.favoritePrimes .contains(self.viewStore.value.count) {

28:57

This logic isn’t too bad right now, but wouldn’t it be better if we could just say: if self.viewStore.value.isFavorite {

29:15

The view store concept gives us the perfect place to move logic out of our view and into a more isolated, understandable place. All we have to do is change the state that the view store holds onto to be exactly what the view wants. In particular, it wants to know what the current count is and if it is a favorite: public struct IsPrimeModalView: View { struct State: Equatable { let count: Int let isFavorite: Bool } … @ObservedObject var viewStore: ViewStore<State>

30:04

And now the body of the view is happy. But when we construct this view store we need to first scope it to view state: self.viewStore = self.store .scope( value: State( count: $0.count, isFavorite: $0.favoritePrimes.contains($0.count) }, action: { $0 } ) .view

30:54

The prime modal module is building, but this is a lot of logic to manage in a view initializer, so let’s move it out to a constructor on that local State struct. extension IsPrimeModalView.State { init(primeModalState state: PrimeModalState) { self.count = state.count self.isFavorite = state.favoritePrimes.contains(state.count) } }

31:33

And then we can more simply use it to scope our store and then view it: self.viewStore = self.store .scope(value: State.init(primeModalState:), action: { $0 }) .view

31:53

This is nice that we can move logic out of the view and move it to a spot where we are just doing a simple transformation from one version of state into another version of state. Even better, this gives us even more opportunities to skip needless re-renders of our screen. For now this view will only re-compute itself when the state of being a favorite changes. This means some other part of the application could be making lots of mutations to our favorites array, but none of those changes will trigger a re-computation unless it happens to change the current count we are looking at.

32:32

There are more examples of how we can do these kinds of view store transformations. For example, in the counter feature we have the current state struct: public struct CounterFeatureState: Equatable { public var alertNthPrime: PrimeAlert? public var count: Int public var favoritePrimes: [Int] public var isNthPrimeButtonDisabled: Bool public var isPrimeModalShown: Bool }

32:55

This is perfectly fine state for us to have, but some may be think its a little strange that we have a mixture of state that is core to our application’s abstract representation (in particular, the count and favoritePrimes fields), and some state that is more descriptive of what is being shown on the screen (like alertNthPrime , isNthPrimeButtonDisabled and isPrimeModalShown ).

33:20

We can use the concept of the view store to better separate these two flavors of state, if we so desire. Let’s do this for the isNthPrimeButtonDisabled piece of state so that you can see how this goes. Right now this field is named very clearly after what it represents in the UI, and that is really handy because it means the view code uses it in the most simple way possible: .disabled(self.viewStore.isNthPrimeButtonDisabled)

34:11

The value of this field is based on whether or not the the API request for the “nth prime” is in flight. When we are trying to load that information we disable the button, and as soon as we get a response we re-enable it.

34:28

But, what if there were more things that wanted to be disabled while that request is in flight? Maybe we are even going to disable the increment and decrement buttons. Maybe we also want to show a loading indicator while that request is in flight. These are all valid things to do, and we could add a lot more state to represent them: public struct CounterFeatureState: Equatable { public var alertNthPrime: PrimeAlert? public var count: Int public var favoritePrimes: [Int] public var isNthPrimeButtonDisabled: Bool public var isPrimeModalShown: Bool public var isIncrementButtonDisabled: Bool public var isDecrementButtonDisabled: Bool public var isLoadingIndicatorHidden: Bool }

35:31

Alternatively, we could describe the base unit of state that all of these states could be derived from. In particular, the thing we really care about is that the network request is in flight, and from there we can derive everything else. So let’s get rid of these UI-specific fields and replace it with the UI-agnostic field: public struct CounterFeatureState: Equatable { public var alertNthPrime: PrimeAlert? public var count: Int public var favoritePrimes: [Int] public var isNthPrimeRequestInFlight: Bool public var isPrimeModalShown: Bool … public init( alertNthPrime: PrimeAlert? = nil, count: Int = 0, favoritePrimes: [Int] = [], isNthPrimeRequestInFlight: Bool = false, isPrimeModalShown: Bool = false ) { self.alertNthPrime = alertNthPrime self.count = count self.favoritePrimes = favoritePrimes self.isNthPrimeRequestInFlight = isNthPrimeRequestInFlight self.isPrimeModalShown = isPrimeModalShown }

36:19

This breaks a few things. The first thing we see is that the counter property on CounterFeatureState , which is responsible for slicing off the only properties that the counter feature cares about (as opposed to the prime modal feature). This feature’s reducer should also stop worrying about whether or not the button is disabled, and instead work with the higher-level concept of whether or not the network request is inflight: public typealias CounterState = ( alertNthPrime: PrimeAlert?, count: Int, isNthPrimeRequestInFlight: Bool, isPrimeModalShown: Bool ) … var counter: CounterState { get { ( self.alertNthPrime, self.count, self.isNthPrimeRequestInFlight, self.isPrimeModalShown ) } set { ( self.alertNthPrime, self.count, self.isNthPrimeRequestInFlight, self.isPrimeModalShown ) = newValue } }

36:39

And then the reducer needs to use this new property: case .nthPrimeButtonTapped: state.isNthPrimeRequestInFlight = true … case let .nthPrimeResponse(prime): … state.isNthPrimeRequestInFlight = false …

36:52

And finally we have the view to fix, and this is where things get interesting. The view store currently expresses exactly the state our view wants, so there’s nothing to change there: struct State: Equatable { let alertNthPrime: PrimeAlert? let count: Int let isNthPrimeButtonDisabled: Bool let isPrimeModalShown: Bool } @ObservedObject var viewStore: ViewStore<State>

37:05

We just need to update the initializer that converts to the view store’s state: extension CounterView.State { init(counterFeatureState state: CounterFeatureState) { self.alertNthPrime = state.alertNthPrime self.count = state.count self.isNthPrimeButtonDisabled = state.isNthPrimeRequestInFlight self.isPrimeModalShown = state.isPrimeModalShown } }

37:18

And now this module is compiling, and we have successfully made the state of this feature more agnostic to the UI while still allowing the view to use a domain-specific description of the state. In particular, we could create a few more booleans to control the enabled/disabled state of the increment and decrement buttons: struct State: Equatable { let alertNthPrime: PrimeAlert? let count: Int let isNthPrimeButtonDisabled: Bool let isPrimeModalShown: Bool let isIncrementButtonDisabled: Bool let isDecrementButtonDisabled: Bool } … extension CounterView.State { init(counterFeatureState state: CounterFeatureState) { self.alertNthPrime = state.alertNthPrime self.count = state.count self.isNthPrimeButtonDisabled = state.isNthPrimeRequestInFlight self.isPrimeModalShown = state.isPrimeModalShown self.isIncrementButtonDisabled = state.isNthPrimeRequestInFlight self.isDecrementButtonDisabled = state.isNthPrimeRequestInFlight } } … Button("-") { self.store.send(.counter(.decrTapped)) } .disabled(self.viewStore.value.isDecrementButtonDisabled) Text("\(self.viewStore.value.count)") Button("+") { self.store.send(.counter(.incrTapped)) } .disabled(self.viewStore.value.isIncrementButtonDisabled)

38:31

What’s cool about this is that we could add these new features and capabilities to our view without needing to make any changes to our reducer or application state. This logic is purely a concern of the view since it’s a pure transformation of application state into view state.

38:53

And we’ve now seen that the ViewStore abstraction is a little more interesting than just being a tool for performance. It is a way for us to mold the view’s data so that it’s in the perfect shape to have the body of our view be as simple and logic-less as possible. Next time: adaptive actions

39:09

However, if there’s one thing we hope you’ve learned on Point-Free it’s that when you have complimentary concepts, such as state and action or struct and enum, as soon as you find something handy for one concept you should immediately look for the equivalent on the other concept. In general that’s just a great principle to live by. And currently our ViewStore is kind of lopsided, in that we are only focusing on the application state when we use a view store. That’s understandable since the whole motivation for the view store was to minimize what state our views know about in order to improve performance, but there’s this other side of our application: the actions!

39:47

By extending our notion of the view store to also account for the actions that a view cares about we will be able to further chisel away at the domain that the view has access to. To see why that would be useful, let’s take a look at the CounterView again…next time! Downloads Sample code 0095-adaptive-state-management-pt2 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 .