EP 99 · Ergonomic State Management · Apr 20, 2020 ·Members

Video #99: Ergonomic State Management: Part 2

smart_display

Loading stream…

Video #99: Ergonomic State Management: Part 2

Episode: Video #99 Date: Apr 20, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep99-ergonomic-state-management-part-2

Episode thumbnail

Description

We’ve made creating and enhancing reducers more ergonomic, but we still haven’t given much attention to the ergonomics of the view layer of the Composable Architecture. This week we’ll make the Store much nicer to use by taking advantage of a new Swift feature and by enhancing it with a SwiftUI helper.

Video

Cloudflare Stream video ID: 1062e2e3043786888afc1fa9ea178dd2 Local file: video_99_ergonomic-state-management-part-2.mp4 *(download with --video 99)*

Transcript

0:21

There’s another thing we can do to improve the ergonomics of our architecture, and that’s in the view layer. Right now, a view holds onto a view store that contains all of the state it cares about to render itself, and in order to access this state, we dive through the view store’s value property. Dynamic member lookup

0:49

If we take a look at CounterView , we’ll see 7 lines that all repeat .value in order to get to the data they care about: .disabled(self.viewStore.value.isDecrementButtonDisabled) … Text("\(self.viewStore.value.count)") … .disabled(self.viewStore.value.isIncrementButtonDisabled) … Button(self.viewStore.value.nthPrimeButtonTitle) { … .disabled(self.viewStore.value.isNthPrimeButtonDisabled) … isPresented: .constant(self.viewStore.value.isPrimeModalShown), … item: .constant(self.viewStore.value.alertNthPrime)

1:05

This repetitive noise isn’t the worst thing in the world, but I think we have the opportunity to take advantage of a Swift feature to streamline things a bit, and that’s “dynamic member lookup.” Dynamic member lookup allows you to enhance a type with the ability to accept dot-syntax calls for properties that don’t live directly on the type.

1:26

Let’s hop over to a playground to see it in action.

1:36

Let’s say we have a simple User struct. struct User { var id: Int var name: String var bio: String } let user = User( id: 1, name: "Blob", bio: "Blobbed around the world" )

1:41

If we wanted to add some admin functionality, we could maybe enhance the User type with an additional field: struct User { var id: Int var name: String var bio: String var isAdmin: Bool } let blob = User( id: 1, name: "Blob", bio: "Blobbed around the world", isAdmin: true ) let blobJr = User( id: 2, name: "Blob Jr", bio: "Blobbed around the world", isAdmin: false )

2:09

And we can guard against any admin-only functionality using this field. func doAdminStuff(user: User) { guard user.isAdmin else { return } print("\(user.name) is an admin") } doAdminStuff(user: blob) // prints "Blob is an admin" doAdminStuff(user: blobJr) // prints nothing

2:49

It might be better, though, to fully distinguish admins as their own type and wrap more basic user data: struct User { var id: Int var name: String var bio: String // var isAdmin: Bool } struct Admin { var user: User }

3:09

Then we can require our function take an Admin value instead of a User . func doAdminStuff(user: Admin) { // guard user.isAdmin else { return } print("\(user.user.name) is an admin") }

3:24

And now we can guard admin-only functionality at compile-time! We can, for example, introduce a function that only accepts Admin s. doAdminStuff(user: blob) doAdminStuff(user: blobJr) Cannot convert value of type ‘User’ to expected argument type ‘Admin’

3:31

We are now forced to have an Admin value at hand in order to execute this functionality. let blob = Admin( user: User( id: 1, name: "Blob", bio: "Blobbed around the world", isAdmin: true ) ) … doAdminStuff(user: blob) // prints "Blob is an admin" // doAdminStuff(user: blobJr)

3:45

And this kind of type-safety is a really good thing. We’ve even explored it in a past episode where we introduced and open-sourced the Tagged type. We highly recommend checking out that episode if you’re unfamiliar.

3:57

Unfortunately, the downside to wrapper types like Admin is that they can come with a lot of boilerplate. To access any of an admin’s user fields, we must traverse into its user field. print("\(admin.user.name) is an admin")

4:20

It’d be nice to instead access these properties directly on the wrapper type: print("\(admin.name) is an admin") Value of type ‘Admin’ has no member ‘name’

4:29

Well, dynamic member lookup allows us to do just that!

4:33

To enhance the Admin type with this functionality we can annotate it with the @dynamicMemberLookup attribute. @dynamicMemberLookup struct Admin { @dynamicMemberLookup attribute requires ‘Admin’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a key path

4:42

It has a single requirement: a subscript that takes a key path. subscript(dynamicMember keyPath: KeyPath<<#???#>, <#???#>>) -> <#???#>

5:08

Implementing this subscript surfaces all of the properties of the key path’s root directly on the wrapper type. So in order to surface all of the User type’s field directly on Admin , the root of the key path should be User and the value should go to some generic type A . subscript<A>(dynamicMember keyPath: KeyPath<User, A>) -> A

5:38

And in the body we can pass the key path along to the user’s key path subscript. subscript<A>(dynamicMember keyPath: KeyPath<User, A>) -> A self.user[keyPath: keyPath] }

5:45

There’s a lot going on here in just a few lines of code, but what it says is that when this subscript is called with a key path, it can forward it to an underlying value, in this particular case the user. And now our doAdminStuff function is compiling and running again, but we should note that accessing any underlying value on the admin’s user works just as well. blob.id // 1 blob.name // "Blob"

6:27

And this is really cool! We were able to flatten a bunch of nested calls and eliminate noise. The Admin type now behaves almost exactly like User , but it is its own, distinguishable type. It’s almost as if we’ve gotten the benefits of class inheritance but without all of the baggage of reference types. Dynamic member store

6:48

So how might dynamic member lookup improve the ergonomics of our architecture? Well, the ViewStore type is a wrapper as well! It holds onto a value field that represents application state. public final class ViewStore<Value, Action>: ObservableObject { … @Published public private(set) var value: Value

7:07

We can start by annotating the store. @dynamicMemberLookup public final class ViewStore<Value, Action>: ObservableObject { @dynamicMemberLookup attribute requires ‘Store’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a keypath

7:13

And then we can implement the subscript: public subscript<LocalValue>( dynamicMember keyPath: KeyPath<Value, LocalValue> ) -> LocalValue { return self.value[keyPath: keyPath] }

7:48

The implementation looks a lot like the one we implemented on Admin , where we merely pass the key path along to the wrapped value’s subscript.

7:55

In our views we should now be able to remove all of the nested calls through value we were making.

8:00

The counter view has a bunch we can fix. public struct CounterView: View { … public var body: some View { print("CounterView.body") return VStack { HStack { Button("-") { self.viewStore.send(.decrTapped) } .disabled(self.viewStore.isDecrementButtonDisabled) Text("\(self.viewStore.count)") Button("+") { self.viewStore.send(.incrTapped) } .disabled(self.viewStore.isIncrementButtonDisabled) } Button("Is this prime?") { self.viewStore.send(.isPrimeButtonTapped) } Button(self.viewStore.nthPrimeButtonTitle) { self.viewStore.send(.nthPrimeButtonTapped) } .disabled(self.viewStore.isNthPrimeButtonDisabled) } .font(.title) .navigationBarTitle("Counter demo") .sheet( isPresented: .constant(self.viewStore.isPrimeModalShown), onDismiss: { self.viewStore.send(.primeModalDismissed) } ) { IsPrimeModalView( store: self.store.scope( value: { ($0.count, $0.favoritePrimes) }, action: { .primeModal($0) } ) ) } .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in Alert( title: Text(alert.title), dismissButton: .default(Text("OK")) { self.viewStore.send(.alertDismissButtonTapped) } ) } .frame( minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity ) .background(Color.white) .onTapGesture(count: 2) { self.viewStore.send(.doubleTap) } } }

8:27

But instead of applying these changes to every view by hand, we can use find–replace to update everything.

8:46

Now these are small, but nice changes, and it will multiply with the size of a code base. It reads nicely, too, like we’re asking the store directly for some state it contains. And it took basically no work. So it’s a handy trick to keep in mind whenever you’re dealing with a wrapper type. Bindings and the architecture

9:16

So dynamic member look up helped make our views less noisy but getting rid of all the viewStore.value repetition, which is nice. There’s one more messy thing in our views that I think we can clean up to make our views even simpler, and it’s bindings.

9:40

Bindings are used in SwiftUI a lot as a form of 2-way communication between two components. They are really powerful, and allow you to get things done very quickly, but they also come with a bit of a complexity cost. Let’s take a look at a few of the bindings we have used so far in our application.

9:56

We currently have 5 bindings in our application. One in the favorite primes screen, two in the counter screen, but then also another two in the counter screen for our macOS application. The binding in the favorite primes screen is used to drive showing and dismissing an alert: .alert(item: .constant(self.viewStore.alertNthPrime)) { primeAlert in Alert( title: Text(primeAlert.title), dismissButton: Alert.Button.default(Text("OK"), action: { self.viewStore.send(.alertDismissButtonTapped) } ) )

10:13

Here we are using a constant binding so that the alert is shown when alertNthPrime becomes non- nil , and then is hidden once it becomes nil . We need to use a constant binding because the alert should not be allowed to change alertNthPrime directly. In the Composable Architecture, the only way to make a change to the state is by sending an action. In fact, this alertNthPrime value on the view store isn’t even writable, so we really have no choice.

10:46

But, at the end of the day, we do need to update our state to nil out the alert, like when they tap the “OK” button on the alert. And that is why when we add the dismiss button to the alert we tap into its action closure so that we can send the alertDismissButtonTapped action. That keeps our state in sync with SwiftUI.

11:07

In the counter view we have something similar, where we have yet another binding that is used to drive an alert: .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in Alert( title: Text(alert.title), dismissButton: .default(Text("OK")) { self.viewStore.send(.alertDismissButtonTapped) } ) }

11:28

Just above this binding we show a modal based on some state: .sheet( isPresented: .constant(self.viewStore.isPrimeModalShown), onDismiss: { self.viewStore.send(.primeModalDismissed) } ) { This makes the modal shown once isPrimeModalShown becomes true, and then is dismissed once isPrimeModalShown goes to false. And further, when the modal is dismissed, this closure is invoked, and rather than trying to mutate our state directly we just send an action off to the view store so that our reducers can take care of it.

11:47

And finally, over in the macOS view we have something similar, except here a popover is powered by the binding, as well as an alert: .popover( isPresented: Binding( get: { self.viewStore.isPrimePopoverShown }, set: { _ in self.viewStore.send(.primePopoverDismissed) } ) ) { .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in

12:14

There is quite a bit of variety of approaches in just these examples. Sometimes we use the .constant binding and then tap into an external event to send a dismissal action, and other times we construct the binding from scratch by implementing the getter to return state from our view store and the setter sends an action to the view store.

12:33

And to be honest, some of these approaches are not quite right 😬. For example, when we use the .constant binding we are basically ignoring any writes that are made to the binding. But there may be times that writes to this binding are actually legitimate.

12:47

For example, right now we handle the dismissal of the “nth prime” alert by sending an action with the dismiss button is tapped: .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in Alert( title: Text(alert.title), dismissButton: .default(Text("OK")) { self.viewStore.send(.alertDismissButtonTapped) } ) }

12:57

This allows our reducer to clear the state associated with the alert, which makes the alert go away in the UI.

13:05

However, we may forget to tap into this dismissal action. Months from now, when we are less familiar with all the internals of how the Composable Architecture works, we may be tempted to just do something like this: .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in Alert( title: Text(alert.title), dismissButton: .default(Text("OK")) ) }

13:25

Or even worse, turns out if you don’t supply any alert buttons you get an “OK” dismissal for free: .alert( item: .constant(self.viewStore.alertNthPrime) ) { alert in Alert(title: Text(alert.title)) }

13:37

If we were to implement our alert like this we are completely missing out on the action that causes the alert to dismiss, which means we are never cleaning up our state. To see why this is problematic, let’s run the app, drill down into the counter screen, trigger the alert, back out to the root screen, and then drill back down into the counter screen. If we do any action on this screen we will suddenly get an alert. And this is because in our application’s state there is still a non- nil value for alertNthPrime , which means it will keep re-triggering and we will never be able to fully dismiss it.

14:26

So, that is the problem with bindings and the Composable Architecture. On the one hand, bindings are super useful and SwiftUI uses them a ton in order for you to control UI components. But, on the other hand they don’t exactly fit into the unidirectional data flow technique that the Composable Architecture employs, and so working around that leads us to using bindings in a potentially unsafe way. Binding helpers

14:48

The solution is to create helpers that allow us to derive honest bindings from the types in the Composable Architecture. Although we don’t like the outside world to change our application’s state directly, we can still cook up bindings that make it look like we are setting state directly, but instead it’s sending actions under the hood.

15:06

For inspiration of what this helper might look like at the call site, let’s look at the binding we use for the popover in our macOS application: .popover( isPresented: Binding( get: { self.viewStore.isPrimePopoverShown }, set: { _ in self.viewStore.send(.primePopoverDismissed) } )

15:21

This is actually the most correct binding we have in our application because it properly handles both the getter and the setter of the binding. There is no way for our view store’s state to get out of sync with SwiftUI because we are making sure to send an action to the store anytime the binding changes.

15:38

There’s still some boilerplate and repeated code in this little snippet. In both the get and the set we are accessing the viewStore , and confusingly in the set we aren’t actually setting anything at all, we are just sending an action. What if instead we could derive bindings from our view store as long as we give it a way to get and send instead of get and set. It might look something like this: .popover( isPresented: self.viewStore.binding( get: { $0.isPrimePopoverShown }, send: { _ in .primePopoverDismissed } )

16:35

This makes it very clear that when the binding needs to get its value it is going through the view store’s state, and when the binding needs to sending a value it is sending this action.

16:51

Better yet, now that Swift 5.2 is out we can even just us a key path for the getter: .popover( isPresented: self.viewStore.binding( get: \.isPrimePopoverShown, send: { _ in .primePopoverDismissed } )

16:57

And even better, if we don’t need access to the value being set we could just supply the action that we want to send when the binding is set directly: .popover( isPresented: self.viewStore.binding( get: \.isPrimePopoverShown, send: .primePopoverDismissed )

17:10

And now that is super succinct, and it protects us against the edge cases of accidentally forgetting to send an action when the binding changes.

17:20

So let’s try implementing this. We know we want this helper method to be on ViewStore since that’s the object that is actually capable of reading state and sending actions: extension ViewStore { public func binding( ) { } }

17:38

We need to introduce a generic for the local value that will be used for this binding. For example, the alert had local state of alertNthPrime and the popover binding would have local state of a boolean: extension ViewStore { public func binding<LocalValue>( ) { } }

17:52

This method needs to be provided two arguments, one for describing how to get the local value from the view store’s value, and one for describing what action to send when the binding is set: extension ViewStore { public func binding<LocalValue>( get: (Value) -> LocalValue, send action: Action ) { } }

18:06

And we want to return a binding from this method: extension ViewStore { public func binding<LocalValue>( get: (Value) -> LocalValue, send action: Action ) -> Binding<LocalValue> { } }

18:12

And finally we can implement the method: extension ViewStore { public func binding<LocalValue>( get: @escaping (Value) -> LocalValue, send action: Action ) -> Binding<LocalValue> { Binding( get: { get(self.value) }, set: { _ in self.send(action) } ) } }

18:48

And with that, our pseudocode in the macOS application is now compiling, and we have our binding helper!

19:05

And although this helper works well for the times that we don’t need the value that is being set, there are times when we actually do want the value. So we can make another overload that allows us to specify an action based on the value being set: public func binding<LocalValue>( get: @escaping (Value) -> LocalValue, send toAction: @escaping (LocalValue) -> Action ) -> Binding<LocalValue> { Binding( get: { get(self.value) }, set: { self.send(toAction($0)) } ) }

19:54

So, let’s use our new binding helper to convert all our existing ad hoc bindings and get everything looking the same. Over in the favorite primes screen we can switch our constant binding out for something that is driven by the store. .alert( // item: .constant(self.viewStore.alertNthPrime) item: self.viewStore.binding( get: \.alertNthPrime, send: .alertDismissButtonTapped ) ) { primeAlert in Alert( title: Text(primeAlert.title), dismissButton: .default(Text("OK")) { self.viewStore.send(.alertDismissButtonTapped) } ) }

20:30

Using this helper is better because we can now react to anything that gets sent to this binding by feeding it back into the store, although it’s looking a lot more verbose. However, if you remember we can now eliminate a bunch of other code, including the dismiss button action, and really, the dismiss button itself. .alert( item: self.viewStore.binding( get: \.alertNthPrime, send: .alertDismissButtonTapped ) ) { primeAlert in Alert(title: Text(primeAlert.title)) }

20:54

In the counter screen for iOS we can change our alert binding: .alert( item: self.viewStore.binding( get: \.alertNthPrime, send: .alertDismissButtonTapped ) ) { alert in

21:19

Which fixes the bug we saw earlier, where alert state wasn’t getting unset, causing alerts to redisplay when views reappeared.

21:45

One other binding in this screen can be updated to the new style, as well. .sheet( isPresented: self.viewStore.binding( get: \.isPrimeModalShown, send: Action.primeModalDismissed ) ) {

22:18

And these helpers can be used in many more situations that just showing alerts, popovers and modals. There are many SwiftUI components that are driven off of bindings, for example text fields, switches, pickers, sliders, steppers, tab bars, and even navigation links. All of those components can be properly integrated with the Composable Architecture by using this binding helper. What’s the point?

22:48

This is probably a good time to step back and reflect on these changes. It’s typically where we may ask “what’s the point?” in order to bring some of our more abstract concepts down to earth. Today’s episode was already grounded, though, so maybe the better question is: were these changes necessary? Couldn’t we have happily used the library as it had been defined at the start of the episode?

23:58

Till next time! Downloads Sample code 0099-ergonomic-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 .