EP 159 · Safer, Conciser Forms · Sep 6, 2021 ·Members

Video #159: Safer, Conciser Forms: Part 2

smart_display

Loading stream…

Video #159: Safer, Conciser Forms: Part 2

Episode: Video #159 Date: Sep 6, 2021 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep159-safer-conciser-forms-part-2

Episode thumbnail

Description

We just made the Composable Architecture’s concise binding helpers safer, but can we make them even more concise? We’ll start with a suggestion that came from the community and employ even more Swift tricks, like dynamic member lookup, to get things even conciser than vanilla SwiftUI.

Video

Cloudflare Stream video ID: 36830b6087bbde7986cc20ff1cb52928 Local file: video_159_safer-conciser-forms-part-2.mp4 *(download with --video 159)*

References

Transcript

0:05

So, everything works exactly as it did before, but we are now allowed to opt into which fields should be bindable. This makes our domain modeling much safer by allowing us to restrict the ways certain state is allowed to be mutated.

0:20

With this in place we are now in a position to reduce even more boilerplate in our binding tools. We’ve already made great strides by not forcing us to create an action for every single piece of state that wants to be mutated via a UI binding, but we can go further.

0:37

Right now constructing a binding to a piece of state is a bit of a pain. We have to invoke the binding method on ViewStore , which requires passing the key path as well as the enum case of our actions that identifies where the binding action resides. Usually this can all fit on a single line, albeit a pretty intense line, but we personally like to mix in copious amounts of newlines to break up our code and so for us this binding helper makes our code quite a bit longer.

1:05

Well, luckily there’s a better way. This idea was first brought to our attention by one of our viewers, June Bash , and then over time independently by a bunch of other viewers on Twitter and in our library’s GitHub discussions . Coupling their ideas with the work we have just done on the @BindableState wrapper we can shorten our code tremendously, and even restore some of the concise ergonomics that we know and love from vanilla SwiftUI. Reducing boilerplate with a protocol

1:33

If we take another look at the current binding helper on ViewStore that works with BindingAction s we will see why it needs to take two arguments: extension ViewStore { func binding<Value>( keyPath: WritableKeyPath<State, BindableState<Value>>, send action: @escaping (BindingAction<State>) -> Action ) -> Binding<Value> where Value: Equatable { self.binding( get: { $0[keyPath: keyPath].wrappedValue }, send: { action(.set(keyPath, $0)) } ) } }

1:44

The first argument tells us what piece of state we want to read from, and the second argument tells us how to send binding actions when something in the binding changes. The second argument is just a function whose purpose is to describe how to embed binding actions into our domains actions, which is usually just a simple case function.

2:05

What if we could implicitly infer when an action holds a BindingAction on the inside rather than passing it around explicitly? This is exactly what protocols allow you to do. We can design a protocol that allows one to indicate whether their action type supports binding actions or not, which has the same shape as the requirement we saw above: protocol BindableAction { associatedtype State static func binding(_: BindingAction<State>) -> Self }

2:52

Since the static requirement binding matches the exact name and signature of the binding case in our SettingsAction enum we can already conform to this protocol without doing any additional work: enum SettingsAction: BindableAction, Equatable { … }

3:25

And already we can provide a small improvement to one of our APIs using this new BindableAction protocol. Currently when invoking the .binding method on a reducer we have to specify which case of the action enum holds the binding action: .binding(action: /LoginAction.binding)

3:43

But actions that conform to the BindableAction protocol already know how to find the binding action in the enum. There’s no need to specify it explicitly.

3:51

This means we can write an overload that doesn’t take any arguments whatsoever: extension Reducer where Action: BindableAction, State == Action.State { func binding() -> Self { Self { state, action, environment in guard let bindingAction = (/Action.binding).extract(from: action) else { return self.run(&state, action, environment) } bindingAction.setter(&state) return self.run(&state, action, environment) } } }

4:40

And now we get to invoke it simply by: .binding()

4:50

This protocol also allows us to write an overload of binding on ViewStore that only works with the view store’s action conforms to BindableAction . In this situation we can drop the send argument entirely since it can be implicitly provided by the protocol requirement: extension ViewStore { func binding<Value>( keyPath: WritableKeyPath<State, BindableState<Value>> ) -> Binding<Value> where Value: Equatable, Action: BindableAction, Action.State == State { self.binding( get: { $0[keyPath: keyPath].wrappedValue }, send: { .binding(.set(keyPath, $0)) } ) } }

5:56

With this defined we can derive bindings from the view store in a much more succinct way by completely omitting the send argument: TextField( "Display name", text: viewStore.binding(keyPath: \.$displayName) // viewStore.binding( // keyPath: \.$displayName, // send: SettingsAction.binding // ) )

6:33

But, if deriving a binding from a view store has now been boiled down to a method that takes a single argument, which is a key path, then what are the chances we can omit the method entirely by leveraging dynamic member lookup? TextField( "Display name", text: viewStore.$displayName // viewStore.binding(keyPath: \.$displayName) // viewStore.binding( // keyPath: \.$displayName, // send: SettingsAction.binding // ) ) Correction While we leverage dynamic member lookup in this episode, we have since adopted a ViewStore.binding overload in the library, instead, due to a subtle behavior involving nested state. For more information on the problem and change, see this release and this pull request .

7:00

If this were possible, then deriving a binding from a view store would look almost identical to deriving a binding from an observable object.

7:11

To do this we need to add a dynamic member subscript to the view store that works only for when the view store’s action is a BindableAction , and only works with key paths into BindableState : extension ViewStore { subscript<Value>( dynamicMember keyPath: WritableKeyPath<State, BindableState<Value>> ) -> Binding<Value> where Action: BindableAction, Action.State == State, Value: Equatable { self.binding( get: { $0[keyPath: keyPath].wrappedValue }, send: { .binding(.set(keyPath, $0)) } ) } }

8:46

And just like that the theoretical syntax of doing viewStore.$displayName is compiling, and it’s so short that they all fit nicely on single lines. Let’s quickly update all of the bindings. struct TCAFormView: View { let store: Store<SettingsState, SettingsAction> var body: some View { WithViewStore(self.store) { viewStore in Form { Section(header: Text("Profile")) { TextField("Display name", text: viewStore.$displayName) Toggle("Protect my posts", isOn: viewStore.$protectMyPosts) } Section(header: Text("Communication")) { Toggle( "Send notifications", isOn: viewStore.$sendNotifications ) if viewStore.sendNotifications { Toggle("Mobile", isOn: viewStore.$sendMobileNotifications) Toggle("Email", isOn: viewStore.$sendEmailNotifications) Picker("Top posts digest", selection: viewStore.$digest) { ForEach(Digest.allCases, id: \.self) { digest in Text(digest.rawValue) } } } } Button("Reset") { viewStore.send(.resetButtonTapped) } } .alert(item: viewStore.$alert) { alert in Alert(title: Text(alert.title)) } .navigationTitle("Settings") } } }

9:32

So now the Composable Architecture version of this view is nearly identical to what we did in the vanilla SwiftUI application. In fact, the whole file is only 17 lines longer if you omit the previews code at the bottom. But also remember that the Composable Architecture version of this code also fully models its dependencies and controls its side effects, making the code much easier to test. So sounds like a huge win overall.

10:03

In fact, the view is even shorter than the vanilla SwiftUI version, because in vanilla SwiftUI we needed to create an explicit binding for the notification toggle. Dollar sign differences

10:34

We’ve now recovered all of the ergonomics and conciseness of vanilla SwiftUI, and then some!

10:51

It’s worth mentioning two small differences between what we have done here and how SwiftUI allows deriving bindings from observable objects. First, you may have noticed that our syntax for deriving a binding looks like this: viewStore.$displayName

11:16

Yet SwiftUI’s looks like this: self.$viewModel.displayName

11:28

There’s a very small difference of where the $ goes. SwiftUI’s goes on the viewModel because that’s what gives you access to the projected value of the observable object, which is an inner wrapper type: self.$viewModel as ObservedObject<SettingsViewModel>.Wrapper

11:42

And it’s that type that supports dynamic member lookup for deriving bindings from any field in the observable object.

11:50

So, you might ask can we support this kind of syntax? After all, iOS 15 has brought some new powers to property wrappers, such us that they are now allowed to be passed as arguments to closures. The prototypical example of this is using a ForEach view on a binding of a collection in order to deriving a binding for each element in the collection: struct MyView: View { @State var array = [1, 2, 3] var body: some View { ForEach(self.$array, id: \.self) { $number in let _ = $number as Binding<Int> let _ = number as Int } } }

12:39

So perhaps we can leverage something similar to do something like this: WithViewStore(self.store) { $viewStore in … TextField("Display name", text: $viewStore.displayName) … }

12:53

If that were possible then the syntaxes would be identical. You place the $ on the viewStore and then just chain onto that to derive bindings.

13:02

Well, unfortunately this is not possible currently, and it’s all due to the work we did that makes us opt into allowing a field to be bindable. We want that safety net, and unfortunately that gets in the way of adopting this syntax.

13:17

In fact, the reason that SwiftUI is able to adopt this syntax is specifically because they are forgoing the safety. One strange thing about how derived bindings work with observable objects is that it allows you to derive bindings to any field. Even if that field is not marked as @Published , which is a little surprising: class SettingsViewModel: ObservableObject { var nonPublishedState = "" … } struct VanillaSwiftUIFormView: View { @ObservedObject var viewModel: SettingsViewModel var body: some View { … TextField("Display name", text: self.$viewModel.nonPublishedState) … } }

13:57

This allows you to erroneously derive a binding to a field whose updates will never trigger the view to recompute. So, unfortunately we can not match the syntax exactly, but we are still quite happy with the end result.

14:28

It’s also worth mentioning something that we brought up a bunch in our last series of episodes covering new WWDC technologies. We built all of these tools outside of the Composable Architecture library. It does not need access to any internals or private implementation details, and is completely additive. This means anyone could have added these ergonomics tools on top of the library themselves, and we’re sure there are lots more tools out there to be discovered.

14:54

In fact let’s revisit a case study we wrote in one of those episodes, where we explored the new @FocusState API. In it, we built a login form in the Composable Architecture, which held some focus state, and we used BindingAction and Reducer.binding to eliminate a lot of boilerplate. Well it sounds like we now have an opportunity to remove even more boilerplate! So let’s copy this case study over to the current project and make some changes. import ComposableArchitecture import SwiftUI struct LoginState: Equatable { var focusedField: Field? = nil var password: String = "" var username: String = "" enum Field: String, Hashable { case username, password } } enum LoginAction { case binding(BindingAction<LoginState>) case signInButtonTapped } struct LoginEnvironment { } let loginReducer = Reducer< LoginState, LoginAction, LoginEnvironment > { state, action, environment in switch action { case .binding: return .none case .signInButtonTapped: if state.username.isEmpty { state.focusedField = .username } else if state.password.isEmpty { state.focusedField = .password } return .none } } .binding(action: /LoginAction.binding) struct TcaLoginView: View { @FocusState var focusedField: LoginState.Field? let store: Store<LoginState, LoginAction> var body: some View { WithViewStore(self.store) { viewStore in VStack { TextField( "Username", text: viewStore.binding( keyPath: \.username, send: LoginAction.binding ) ) .focused($focusedField, equals: .username) SecureField( "Password", text: viewStore.binding( keyPath: \.password, send: LoginAction.binding ) ) .focused($focusedField, equals: .password) Button("Sign In") { viewStore.send(.signInButtonTapped) } Text("\(String(describing: viewStore.focusedField))") } .synchronize( viewStore.binding( get: \.focusedField, send: LoginAction.binding ), self.$focusedField ) } } } extension View { func synchronize<Value: Equatable>( _ first: Binding<Value>, _ second: FocusState<Value>.Binding ) -> some View { self .onChange(of: first.wrappedValue) { second.wrappedValue = $0 } .onChange(of: second.wrappedValue) { first.wrappedValue = $0 } } }

15:59

First, we can mark which properties should be bindable, which in this case is all three of them. @BindableState var focusedField: Field? = nil @BindableState var password: String = "" @BindableState var username: String = ""

16:06

Then, we can conform the action to be bindable, as well: enum LoginAction: BindableAction { … }

16:19

We can drop the argument passed to Reducer.binding : .binding()

16:27

And then in the view, all of our view store bindings get dramatically shorter, including the one we pass to the synchronize helper: VStack { TextField("Username", text: viewStore.$username) .focused($focusedField, equals: .username) SecureField("Password", text: viewStore.$password) .focused($focusedField, equals: .password) Button("Sign In") { viewStore.send(.signInButtonTapped) } } .synchronize(viewStore.$focusedField, self.$focusedField)

17:04

Much nicer! What’s the point?

17:13

So we’ve made quite a bit of headway to making forms safer and more concise, leveraging lots of advanced features of Swift: key paths, type erasure, property wrappers and dynamic member look up. But let’s take a moment to slow down and ask “what’s the point?”

17:41

So far we have been playing with a toy demo application, and we want to show how this can improve large, real world code bases.

17:50

And it just so happens that a few months ago we open sourced a large, multi-screen game written entirely in Swift and the Composable Architecture, called isowords . We make use of these binding helpers in 4 features of the application, and we’d like to take a moment to see how the new improvements to our binding helpers can simplify our existing code base.

18:13

Here we are in the isowords project. If we search the project for “case binding” we will find the 4 features currently using these binding helpers:

18:26

CubePreview : this feature powers a screen that shows the word cube in a particular state, and then autonomously plays a sequence of moves on the cube. This is used in our leaderboards for seeing how our top scorers made the words they did.

18:45

Home : this feature powers the home screen, which collects data from many sources, such as our API and Game Center, to show on the screen.

18:55

Settings : this feature allows the user customize various things in the game, such as sounds, themes, accessibility and more.

19:03

Trailer : this feature allows one to load a cube onto the screen and have it autonomously play itself, which we use to record video for the trailer on the app store and on our website.

19:15

The heaviest user of the binding helpers is the settings feature, so let’s start there. We can set the active scheme to SettingsFeature so that we are only building the code for settings. We will also update the Package.swift file that powers the entire application to point to a new branch we have on the Composable Architecture repo that brings in all of these new binding helpers and deprecates the old, unsafe, less concise ones: .package( url: "https://github.com/pointfreeco/swift-composable-architecture", .branch("better-bindings") )

20:04

With that done, if we build SettingsFeature we will see a whole bunch of deprecation warnings. Looks like about 24. This is because we are using the binding helpers a lot in this feature, but we’re not using any of the new stuff yet. So let’s start!

20:32

We can begin in Settings.swift , which holds the majority of the domain for the whole feature. We can mark the SettingsAction enum as conforming to the BindableAction protocol, which is automatically satisfied due to the fact that we have a case named binding , which serves as the static requirement of the protocol: public enum SettingsAction: BindableAction, Equatable { case binding(BindingAction<SettingsState>) … }

20:41

The mere fact that we conform to that protocol means that we can already simplify our invocation of the .binding higher-order reducer: .binding()

20:58

So that’s cleaned up a small amount, but the real wins come from fixing the deprecation warnings. At the bottom for this file we have 3 warnings because we are constructing 3 alerts, and using the binding helper to nil out the alert when dismissing: primaryButton: .default(.init("OK"), action: .send(.binding(.set(\.alert, nil)))),

21:14

This is currently going through the old, unsafe, deprecated .set method because we haven’t proven to the compiler that the .alert field is marked as BindableState . To do this we can add the annotation to SettingsState : public struct SettingsState: Equatable { @BindableState public var alert: AlertState<SettingsAction>? … }

21:38

And then access the property wrapper by using the $ syntax: primaryButton: .default( .init("OK"), action: .send(.binding(.set(\.$alert, nil))) ),

21:48

And just like that we have fixed 3 compiler warnings, and are beginning to make our settings code safer by being explicit with what fields of SettingsState are allowed to be changed via binding actions.

22:00

Next let’s hop over to the AccessibilitySettingsView , where we currently have 3 deprecation warnings, all due to our usage of the .binding method on ViewStore , for example: Toggle( "Cube motion", isOn: self.viewStore.binding( keyPath: \.userSettings.enableGyroMotion, send: SettingsAction.binding ) )

22:20

This is 7 lines of code for what should be just one. If we mark userSettings as @BindableState in SettingsState : @BindableState public var userSettings: UserSettings

22:40

Then we get to squash all of this code down to a single line: Toggle( "Cube motion", isOn: self.viewStore.$userSettings.enableGyroMotion )

22:53

Similarly for the other two: Toggle("Haptics", isOn: self.viewStore.$userSettings.enableHaptics) … Toggle( "Reduce animation", isOn: self.viewStore.$userSettings.enableReducedAnimation )

23:12

That just turned 21 lines of code into 3.

23:18

There are 11 more examples of this same kind of deprecation, which means nearly 80 lines of code can be squashed into just 11 lines once we fix them all.

23:31

Some warnings can even be reduced further. For example, this code constructs a binding for a slider and then applies an animation to the binding, which currently takes 8 lines: Slider( value: self.viewStore.binding( keyPath: \.userSettings.musicVolume, send: SettingsAction.binding ) .animation(), in: 0...1 )

23:52

With the new binding improvements this all fits on a single line: Slider( value: viewStore.$userSettings.musicVolume.animation(), in: 0...1 )

24:20

So already see that this is going to make a huge difference in the SettingsFeature , but let’s take a look at a different feature.

24:30

In the trailer feature we construct a whole bunch of effects and concatenate them together in order to autonomously drive interactions with the cube. We streamline this by using a binding action in the domain so that we can do simply things like constructing an effect that moves the little pointer indicator to a different location: Effect(value: .binding(.set(\.nub.location, .face(face))))

25:40

This greatly reduces the number of actions we need in the trailer domain, which is already complex enough as it is, but at the cost of opening up all of the feature’s state to being mutated. This is quite the overreach of the binding action because the only things we actually want to mutate from it is the nub state and the opacity.

26:05

So, let’s switch to using the non-deprecated binding helpers to get back some safety. We’ll mark the fields in TrailerState that need to be bindable: public struct TrailerState: Equatable { var game: GameState @BindableState var nub: CubeSceneView.ViewState.NubState @BindableState var opacity: Double … }

26:19

Notice that none of GameState is marked as bindable and so the only way to mutate that state is via explicit actions.

26:29

We’ll also mark TrailerAction as bindable: public enum TrailerAction: BindableAction, Equatable { … }

26:34

And start using the simplified .binding higher-order reducer: .binding()

26:40

And then, each place we are constructing an effect to set the nub’s state or opacity, we just need to insert a $ in order to prove to the compiler that we have marked that property as being bindable: Effect(value: .binding(.set(\.$nub.location, .face(face))))

26:54

And just like that we have made this feature’s domain safer while not sacrificing any of its conciseness. Conclusion

27:19

That concludes this week’s episode. We just wanted to follow up on a topic we covered a few months ago to update it with some ideas that we got from the community as well as a few new things we discovered along the way. Until next time. References Further reducing boilerplate with... a protocol...? June Bash • Feb 1, 2021 June Bash first suggested using a protocol like BindableAction in this GitHub discussion. https://github.com/pointfreeco/swift-composable-architecture/discussions/370 Enum cases as protocol witnesses Suyash Drijan • Jan 18, 2020 The Swift Evolution proposal that made it possible for enums to conform to protocols via their case initializers. https://github.com/apple/swift-evolution/blob/main/proposals/0280-enum-cases-as-protocol-witnesses.md Downloads Sample code 0159-safer-conciser-forms-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 .