EP 107 · Composable SwiftUI Bindings · Jul 6, 2020 ·Members

Video #107: Composable SwiftUI Bindings: The Problem

smart_display

Loading stream…

Video #107: Composable SwiftUI Bindings: The Problem

Episode: Video #107 Date: Jul 6, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep107-composable-swiftui-bindings-the-problem

Episode thumbnail

Description

Bindings are one of the core units of SwiftUI data flow and allow disparate parts of an application to communicate with one another, but they are built in such a way that strongly favors structs over enums. We will show that this prevents us from properly modeling our domains and causes unnecessary complexity in the process.

Video

Cloudflare Stream video ID: d563d9bb9bacaa465c58e446fce76a6d Local file: video_107_composable-swiftui-bindings-the-problem.mp4 *(download with --video 107)*

Transcript

0:27

Many months ago we introduced the concept of “case paths”, which serves the same purpose as key paths do for each field of a struct, but instead they work for each case of an enum. At first it may have seem a little abstract, after all, if case paths are so important why didn’t Apple give us first class support for them in Swift?!

0:50

But, we were able to show that case paths allow us to abstractly pick apart and isolate parts of enum so that we can write generic algorithms over the shape of a data type. So far our biggest application of this idea has been to the concept of transforming reducers in the Composable Architecture. In particular, if we have a reducer that operates on a small domain of user actions we can transform it into a reducer that works on global actions by using a case path.

1:20

That is an example of a generic algorithm that is able to do its job because we hand it a case path for isolating and transforming a single case of an enum. But it’s only the tip of the iceberg, there are lots of applications of case paths just as there are lots of applications of key paths, and today we will start a series of episodes to explore that further.

1:41

The Binding type in SwiftUI is one of the most important types in the framework because it facilitates the communication between disparate parts of your application. It allows a change in one corner of your application to be instantly reflected in another part, and whether you are using an @ObservedObject , a @State property wrapper or an environment value, you are ultimately dealing with bindings.

2:06

And because Binding is such a fundamental concept in SwiftUI we would also hope that it is a super composable and transformable unit. That is, it should be possible to take existing bindings and derive all new ones using some simple operators and constructions. And indeed, SwiftUI does ship with a few ways to transform bindings, and it’s actually incredibly powerful.

2:25

However, unfortunately, the transformations provided are only half of the picture. All of the tools provided to us out of the box are heavily geared towards structs and more generally what are known as “product” types, mostly because the tools leverage the power of key paths. The tools for properly dealing with enums and sum types are simply absent, which means that it is difficult or impossible to model our domains in a precise manner, which leads to a lot of unnecessary complexity.

2:52

Well, soon we will fix this deficiency in SwiftUI’s toolset, and of course case paths are going to play a big part in accomplishing this because they are the perfect tool for generically working with enums.

3:03

But to begin, let’s first get a better understanding of how bindings work and what are the ways we can transform them right out of the box in SwiftUI. Inventory entry screen

3:15

We will be using the following seemingly simple screen to explore this concept. It represents a screen for adding items to an inventory system. You can enter the name of the item, select a color, and then specify its quantity. To make things a little interesting, it isn’t enough to only specify its quantity. When an item is marked as sold out there is the further option that the item is on back order, meaning someday it will be in stock, or it is simply out of stock forever.

3:51

This screen would be quite easy to build in the Composable Architecture , and we love the Composable Architecture, but we want to see what it would take to build in vanilla SwiftUI. We want to do this because most people building SwiftUI applications will be using the primitives that Apple gives us, and we want to show that case paths can enhance these primitives.

4:10

Let’s start with a bit of domain modeling. We want to define a new data type that represents the data on this screen. We’ll start by modeling just the title and color of the item: struct Item { var name: String var color: Color enum Color { case blue case green case black case red case yellow case white } }

4:37

And then we can create a basic form view that exposes UI controls for editing these fields. We’ll create a new view struct that holds onto a binding of an Item : struct ItemView: View { @Binding var item: Item }

4:55

@Binding is one of a few property wrappers SwiftUI provides for managing app state. And we are using it here because this screen doesn’t need to hold a source of truth of what an item is, but rather it can be handed an item that someone else owns, and we just want any mutations we make to it to be reflected in the parent view. This is exactly what bindings excel at.

5:18

To implement the body we can just wrap a text field and a picker inside a form. We will use the item binding in order to derive bindings of parts of the item, which can then be handed to the UI controls so that they can be edited: var body: some View { Form { TextField("Name", text: self.$item.name) Picker(selection: self.$item.color, label: Text("Color")) { } } .navigationBarTitle(Text("Add item")) }

6:30

In order to get access to all of the colors inside the Color enum we will make the type conform to the CaseIterable protocol: enum Color: CaseIterable { … }

7:01

Then in order to use this array of colors with a ForEach , we can make them raw-representable by string, identify them by this raw value, and then tag each row with the hashable color. enum Color: String, Hashable { … } … ForEach(Item.Color.allCases, id: \.rawValue) { color in Text(color.rawValue) .tag(color) }

7:57

But we also want to support the idea of not having a color at all, so let’s make it optional. struct Item { var name: String var color: Color? … }

8:04

And then we can update our view with a row that represents no color and tag it with Optional.none . Picker(selection: self.$item.color, label: Text("Color")) { Text("None") .tag(Item.Color?.none) ForEach(Item.Color.allCases, id: \.rawValue) { color in Text(color.rawValue) .tag(color) } }

8:16

But further, we must update the tag for each row to be explicitly optional for the picker to work with the binding. Picker(selection: self.$item.color, label: Text("Color")) { Text("None") .tag(Item.Color?.none) ForEach(Item.Color.allCases, id: \.rawValue) { color in Text(color.rawValue) .tag(Optional(color)) } }

8:26

This is one of the few cases where the magic of optional promotion comes with some gotchas.

8:38

And with this basic infrastructure set up let’s get a SwiftUI preview going. There’s a complication in that it’s not exactly clear how we construct the binding that needs to be passed to the item view: struct ItemView_Previews: PreviewProvider { static var previews: some View { ItemView(item: <#Binding<Item>#>) } }

8:54

Typically one constructs bindings by deriving them from observed objects or @State , but we don’t have access to any of that here. Another possibility is to construct a mutable item that a binding can manipulate directly: struct ItemView_Previews: PreviewProvider { static var previews: some View { var item = Item(name: "Keyboard", color: .green) let itemBinding = Binding(get: { item }, set: { item = $0 }) return ItemView(item: itemBinding) } }

9:34

We now have something on the screen. One weird thing is that the color row seems to be disabled. It turns out that embedding a picker view into a form automatically sets up the functionality of drilling down to a new screen for making a picker selection. This is really awesome, but also means that it will only work if our view is further embedded in a NavigationView , so let’s add that to the preview: return NavigationView { ItemView(item: itemBinding) }

10:04

And now our preview is running, and it works as expected. We can edit the title and color of an item. Implementing quantity

10:19

Let’s now try implementing the quantity feature of the item editor. This is quite a bit more complicated than the title and color because this state has some inter-dependencies. If the item is in stock then we show a quantity stepper control, and otherwise we show a toggle that shows whether or not the item is on back order or permanently sold out.

10:43

The most important part of implementing this feature is the domain modeling. If we do this part right then we would hope that the UI naturally comes together with little work.

10:54

Let’s start the domain modeling in a very straightforward manner, albeit a little naive. We know the item has a quantity so we’ll add that: struct Item { var name = "" var color: Color? var quantity = 1 }

11:05

And we know that the item can be either in stock or out of stock: struct Item { var name = "" var color: Color? var quantity = 1 var isInStock = true }

11:12

And finally when an item is out of stock it can either be on back order or not: struct Item { var name = "" var color: Color? var quantity = 1 var isInStock = true var isOnBackOrder = false }

11:21

Now some of our viewers may have something tickling in the back of their minds telling them that that isn’t quite right, but we’ll address that soon enough. This is a perfectly fine way to model our domain for a first attempt, so let’s just go with it.

11:35

With some new state added to our model, let’s add the UI controls that can update this new state. We can start by checking if the item is in stock, and if so we will display a stepper for the quantity: if self.item.isInStock { Section(header: Text("In stock")) { Stepper( "Quantity: \(self.item.quantity)", value: self.$item.quantity ) } }

12:19

And otherwise we can show the back ordered toggle: } else { Section(header: Text("Out of stock")) { Toggle("Is on back order?", isOn: self.$item.isOnBackOrder) } }

12:44

We still have no way to switch back and forth between in stock and out of stock. To do that we will introduce a button to mark the item as sold out: if self.item.isInStock { Section(header: Text("In stock")) { Stepper( "Quantity: \(self.item.quantity)", value: self.$item.quantity ) Button("Mark as sold out") { self.item.quantity = 0 self.item.isInStock = false } } } else {

13:07

And another button to mark the item as back in stock: } else { Section(header: Text("Out of stock")) { Toggle(isOn: self.$item.isOnBackOrder) { Text("Is on back order?") } Button("Is back in stock!") { self.item.quantity = 1 self.item.isInStock = true } } }

13:22

And now if we try things out…it doesn’t quite work. We’re not sure why, seems like a SwiftUI or Xcode bug. We’ve also tried this code in the most recent Xcode 12 beta and the bug still persists.

13:47

To work around we can just create a little wrapper view that uses @State and then passes down the binding: struct ItemView_Previews: PreviewProvider { static var previews: some View { struct Wrapper: View { @State var item = Item(name: "Keyboard", color: .green) var body: some View { ItemView(item: self.$item) } } } return NavigationView { Wrapper() } }

14:22

It’s a real bummer that we have to do this dance, but we’re not sure if there’s a better way to work around.

14:25

And now the feature basically works just as we demoed it at the beginning of this episode. Domain modeling

14:37

However, there is something not quite right about how we have built this feature. Sure we got the job done, and it seems mostly fine right now, but we have subtly hidden a vast amount of complexity that will only get worse over time and infect every part of our application that needs to interact with this domain.

14:56

This crux of the problem is these 3 fields in the Item data type: var isInStock = true var isOnBackOrder = false var quantity = 1

15:01

This certainly represents the domain of stock for our item, but it also represents a lot more than we want. For example:

15:12

What does it mean for isInStock to be true when isOnBackOrder is also true ? How can something be on back order when it’s already in stock?

15:21

Similarly, what does it mean for isInStock to be false , yet quantity is greater than 0?

15:30

One way people may try to wrangle in this situation is to provide custom initializers on Item so that we can only construct items that do not represents the weird states we just mentioned: extension Item { static func inStock( name: String, color: Color?, quantity: Int ) -> Self { Item( color: color, quantity: quantity, isInStock: true, isOnBackOrder: false, title: title ) } static func outOfStock( name: String, color: Color?, isOnBackOrder: Bool ) -> Self { Item( color: color, quantity: 0, isInStock: false, isOnBackOrder: isOnBackOrder, title: title ) } }

16:48

Here we have exposed two static functions for constructing items, and each one represents the two cases of either being in stock or out of stock.

16:57

Then, if we were to make the initializer for Item private, there would essentially be no way to construct one of these items without going through one of these static functions, which would give us some confidence that only valid values can be constructed…right!?

17:11

Well, there just is no way to guarantee that. First, even though the initializer may be private it is still accessible to all of the code in this file, which means we are free to construct invalid values and propagate them to code outside this file. Further, there are ways to open up this type to freer initialization, such as if we made the type Decodable . Then we could create Item values from JSON, and we have no ability to make sure only valid JSON representations are used unless we remembered to override the synthesized initializer with more custom logic.

17:44

So, at first it may seem innocent enough to have these small inconsistencies in our data model, but at its root it means you simply cannot trust this data. You can try as hard as you want to make sure that only valid combinations of these fields are ever constructed, but you simply must assume that eventually you will have to deal with data that represents a state your application should never be in.

18:05

We need to remodel our domain with a concentration on completely removing invalid states from our data type. That is, the values that we know are nonsensical, such as being in stock and on back order, are simply not present in the type. They are completely unrepresentable, and the compiler will prove it for us.

18:24

This is a topic we have talked about quite a bit on Point-Free, in particular when we discussed algebraic data types . We built up the theory from scratch, and we highly recommend people go back and watch those episodes if they need a refresher, but the idea is you can utilize structs and enums to whittle away at our data types until just the bare essentials are left.

18:42

We are already attempting to do that by exposing a couple static constructors. What this is really pointing out is that our model should have utilized an enum to represent these two cases rather than an integer and two booleans. So we’ll introduce a new type that represents whether or not the item is in stock, and each case of the enum can hold additional data that is pertinent to that state: // var isInStock = true // var isOnBackOrder = false // var quantity = 1 var status: Status enum Status { case inStock(quantity: Int) case outOfStock(isOnBackOrder: Bool) }

19:35

This will break a bunch of stuff. Let’s start by commenting out the static constructors we added to Item because those are no longer necessary.

19:46

Further our if statements in the view for checking which state the item is in is no longer correct, but it’s going to take a decent amount of work to fix that, so let’s ignore it for a moment.

19:57

We can fix our SwiftUI previews by just updating how we construct the initial item: struct ItemView_Previews: PreviewProvider { static var previews: some View { struct Wrapper: View { @State var item = Item( name: "Keyboard", color: .green, status: .inStock(quantity: 1) ) var body: some View { ItemView(item: self.$item) } } return NavigationView { Wrapper() } } }

20:11

Now let’s look again at our view. We currently have have a simple if / else statement to decide which control we display for the quantity. Right now this isn’t compiling because we no longer have the properties such as isInStock , quantity and isOnBackOrder : if self.item.isInStock { Section(header: Text("In stock")) { Stepper( "Quantity: \(self.item.quantity)", value: self.$item.quantity ) Button("Sold out") { self.item.quantity = 0 self.item.isInStock = false } } } else { Section(header: Text("Out of stock")) { Toggle("Is on back order?", isOn: self.$item.isOnBackOrder) Button("Back in stock!") { self.item.quantity = 1 self.item.isInStock = true } } }

20:27

But better than having those properties we now have a proper enum that can be switched over to handle each of its cases individually. Currently in Swift 5.2 we cannot use a switch statement in views due to a limitation in function builders, but once Swift 5.3 ships, which is only a few months away, we will be able to use switch statements directly in views, and so perhaps that will help us here: switch self.item.status { case let .inStock(quantity): … case let .outOfStock(isOnBackOrder): … } Closure containing control flow statement cannot be used with function builder ‘ViewBuilder’

21:14

Now we’re not using the beta while recording this episode, but we can simulate this feature by wrapping the switch in a Group and explicitly wrapping each case’s view in an AnyView : Group { () -> AnyView in switch self.item.status { case let .inStock(quantity): return AnyView(…) case let .outOfStock(isOnBackOrder): return AnyView(…) } }

21:45

And so now in here we’d hope that we can get each section compiling. In the first section we can use the quantity that was destructured from the enum case and update the buttons to set the status directly. case let .inStock(quantity): return AnyView( Section(header: Text("In stock")) { Stepper("Quantity: \(quantity)", value: self.$item.quantity) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) // self.item.quantity = 0 // self.item.isInStock = false } } ) case let .outOfStock(isOnBackOrder): return AnyView( Section(header: Text("Out of stock")) { Toggle(isOn: self.$item.isOnBackOrder) { Text("Is on back order?") } Button("Is back in stock!") { self.item.status = .inStock(quantity: 1) // self.item.quantity = 1 // self.item.isInStock = true } } } }

22:23

However, this still does not compile because we no longer have easy access to the quantity binding self.$item.quantity or back order binding self.$item.isOnBackOrder . So we are seeing that while it is going to be very cool to get the ability to use switch es in SwiftUI, it doesn’t quite solve the problem we are attacking right now.

22:40

So let’s back out of these changes and go back to our simple if / else statement.

22:49

Another approach is to recreate these properties on our status enum. For example, the isInStock property is quite easy to implement: var isInStock: Bool { guard case .inStock = self else { return false } return true }

23:11

And then in our view we can access the isInStock property on the status field: if self.item.status.isInStock {

23:17

That fixes one compiler error, but there’s a few more left.

23:20

The easiest to fix are the ones in the button action closures where we reset the status based on marking an item sold out or back in stock. Currently we are setting a few fields separately to reset the item into a particular status, but now we can just overwrite the status field to precisely represent the situation: Button("Sold out") { // self.item.quantity = 0 // self.item.isInStock = false self.item.status = .outOfStock(isOnBackOrder: false) } … Button("Back in stock") { // self.item.quantity = 1 // self.item.isInStock = true } self.item.status = .inStock(quantity: 1) }

23:49

Next we have a compiler error because we are accessing fields on the item which no longer exists. For instance, quantity . We can emulate this property easily enough: var quantity: Int { switch self { case .inStock(quantity: let quantity): return quantity case .outOfStock: return 0 } }

24:24

And then we can update the view to access this property on the status : Stepper("Quantity: \(self.item.status.quantity)", …)

24:31

It is a little strange that we have to return an integer from the outOfStock case. Sure 0 is reasonable to return, but it would be even better if we simply didn’t even have to consider what quantity meant for that case, after all that’s why we turned to an enum in the first place. We could maybe return an optional from the property: var quantity: Int? { … case .outOfStock: return nil } }

24:52

But then that complicates using it in the view. We either need to further unwrap that optional: self.item.status.quantity.map { quantity in Stepper("Quantity: \(quantity)", …) }

25:05

Or we need to coalesce it to something non-optional, which just pushes the logic out of the model and into the view: Stepper("Quantity: \(self.item.status.quantity ?? 0)", …)

25:11

Neither of those approaches are very good, so let’s just go back to return an honest integer for now and we’ll address this weirdness in a moment: var quantity: Int { … }

25:21

The next compiler error we have is where we derive the quantity binding to hand to the stepper: value: self.$item.quantity

25:27

We may be tempted to just make sure this accesses the quantity property on status : value: self.$item.status.quantity Cannot assign to property: ‘quantity’ is a get-only property

25:33

But this doesn’t work because quantity is only a getter computed property, and deriving bindings like this requires the property to be a getter and setter. After all, when the stepper is changed we want to make sure that change is ultimately propagated back to the item.

25:51

So it looks like we need to upgrade the getter to also be a setter, but in doing so we need to make a few choices. For example, we could have setting the quantity only update the status when we are already in the isInStock case: var quantity: Int { get { … } set { switch self { case .inStock: self = .inStock(quantity: newValue) case .outOfStock: break } } }

26:23

This kinda makes sense as we should only be editing the quantity when we are already in stock. And in fact, our view somewhat enforces this since we only show the stepper control if we are in stock.

26:33

However, one might say that setting the quantity of an item should just flip us to be in stock even if we are currently out of stock: set { self = .inStock(quantity: newValue) }

26:44

Which one of these is the right choice? I’m not sure honestly. They both have their pros and cons, but let’s just keep moving forward to see how this goes.

26:52

With the setter implemented we finally have this line compiling: Stepper( "Quantity: \(self.item.status.quantity)", value: self.$item.status.quantity )

26:58

Our final compiler error is this line where we try to derive a binding for the isOnBackOrder property: Toggle(isOn: self.$item.isOnBackOrder) { Text("Is on back order?") }

27:03

We no longer have this property, so let’s repeat what we did for the quantity property by adding a getter/setter computed property to Status . We again have to grapple with some weird things, for example when the item is in stock what should we return for the getter? Neither true or false make sense: get { guard case let .outOfStock(isOnBackOrder) = self else { return false // ??? // return true // ??? } return isOnBackOrder }

27:57

We could also make this property return an optional boolean, but that will have the same problems that the optional quantity had, so let’s not do that.

28:08

The setter is also fraught with confusion, because the setter could only set in the case that we are already out of stock: set { switch self { case .inStock: break case .outOfStock: self = .outOfStock(isOnBackOrder: newValue) } }

28:33

This is already being emulated in the view since we only show the toggle when the item is already in the out of stock status. But perhaps we should allow setting this value even when we are in stock, and we’ll just flip to out of stock: set { self = .outOfStock(isOnBackOrder: newValue) }

28:47

Again, I’m not sure which is the right choice, but this will at least get us compiling once we access this property from status : Toggle(isOn: self.$item.status.isOnBackOrder) { Text("Is on back order?") }

28:56

And if we run the preview everything seems to work just as before, and so our view really is updating our state even though we drastically remodeled how the status is structured so that it more precisely describes the values that are possible.

29:29

However, there is clearly something not quite right about what we have done because we made quite a few strange decisions along the way. We created properties on the Status enum to project out certain information from it, but in each case that information only made sense for one of the cases. This forced us to make some choices, and it wasn’t clear that we were making the right choice. And if we ever added a third case to this enum or choices will only increase.

29:53

So what we are seeing here is that although we better modeled our core domain to properly use an enum that precisely describes the two states our item can be in, we have also accidentally destroyed all of that preciseness by trying to project out state-specific information from the general enum for our views.

30:13

And this is happening for one really important reason: quite simply, Swift favors structs over enums.

30:19

This is something we have talked about a ton on Point-Free. Swift gives first class support of many concepts and techniques for structs for which there is no corresponding story for enums. We have explained over and over again that structs and enums are really just two sides of the same coin, and any concept we introduce for one we should try searching for the corresponding concept for enums.

30:41

In this case what we are seeing is that SwiftUI simply does not give us the tools for dealing with state that is modeled as an enum. All of the tools it gives us are heavily embedded in the world of structs and product types, which leads us to trying to shoehorn tools made for structs into the world of enums.

30:59

And so without those tools we keep instinctively turning to methods of classic encapsulation to preserve invariants of our model rather than fully leveraging the benefits of structs and enums to make invalid states of unrepresentable. The idea of encapsulation is drilled into us at a very early stage as programmers as the proper way to manage complexity, but here we are seeing that even if we try to put a nice public interface over the core model we can still have complexity leak out and infect our view. Next time: binding transformations

31:30

So, how do we fix this? Well, we need to pick up where Swift and SwiftUI left off, which means creating tools that allow us to use enums in places that were only designed to be used with structs. To understand what this could possibly look at, let’s take a deeper look at what exactly is the tool that SwiftUI gives us that we claim is so tailored to only structs. Downloads Sample code 0107-composable-bindings-pt1 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 .