Video #108: Composable SwiftUI Bindings: Case Paths
Episode: Video #108 Date: Jul 13, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep108-composable-swiftui-bindings-case-paths

Description
Now that we know that SwiftUI state management seems biased towards structs, let’s fix it. We’ll show how to write custom transformations on bindings so that we can use enums to model our domains precisely without muddying our views, and it turns out that case paths are the perfect tool for this job.
Video
Cloudflare Stream video ID: df4b4947c11449a1fad87d324c50d190 Local file: video_108_composable-swiftui-bindings-case-paths.mp4 *(download with --video 108)*
References
- Discussions
- that library
- 0108-composable-bindings-pt2
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
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.
— 0:29
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.
— 0:49
And this is happening for one really important reason: quite simply, Swift favors structs over enums.
— 0:55
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.
— 1:17
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.
— 1:35
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. Binding transformations
— 2:05
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.
— 2:31
It’s hard to see just how much SwiftUI prefers structs because the compiler comes with some fancy features and syntactic sugar that hide away a lot of complexity. But when we write the following line: self.$item.status.quantity
— 2:47
A few things are happening. First we are getting access to the binding that powers the item field of our view using this syntax: (self.$item as Binding<Item>)
— 2:54
And this is only possible because we’ve used the @Binding property wrapper: @Binding var item: Item
— 2:57
So, once we have a binding of an Item we can further dot-chain onto it using a property of the Item to derive another binding: (self.$item.status as Binding<Item.Status>)
— 3:00
It’s worth nothing that the Binding type certainly doesn’t have a .status property on it. We are only able to do this due to the magic of “dynamic member lookup”, which allows us to use dot-chaining on types with fields that do not actually exist, but under the hood we can do logic to re-route that property access to something else.
— 3:29
We can even continue dot-chaining onto this to derive yet another binding: (self.$item.status.quantity as Binding<Int>)
— 3:35
Now we know that “dynamic member lookup” is giving us this magical ability to keep deriving new bindings from existing bindings by simply dot-chaining, but what is the actual shape of this transformation?
— 3:46
Let us for a moment forget we have the “dynamic member lookup”. Even if Apple had never handed down this magical functionality from on high, we would have most definitely ended up implementing it ourselves.
— 4:00
Let’s try implementing it from scratch to see what is actually involved. The crux of “dynamic member lookup” is that we have a binding on some value and we want to derive a binding from it that focuses in on a smaller part of the value, and we do this via a key path. We can codify this explanation into an actual function signature: extension Binding { func transform<LocalValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { <#???#> } }
— 4:48
This function says we have a key path that goes from Value to a LocalValue , and using it we want to transform our Binding of Value into a Binding of LocalValue .
— 5:09
We know we need to return a binding, so we can start there: extension Binding { func transform<LocalValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { Binding<LocalValue>( get: { <#???#> }, set: { <#???#> } ) } }
— 5:08
In this get argument we need to produce something of type LocalValue . The only things we have at our disposal is the current binding on Value as well as the key path. The current binding gives the ability to get at the underlying wrapped value, and then we can further key path into that value to extract out a LocalValue : extension Binding { func transform<LocalValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { Binding<LocalValue>( get: { self.wrappedValue[keyPath: keyPath] }, set: { <#???#> } ) } }
— 5:25
Next, the set argument is a closure that hands us a new value that we need to do some kind of setting with. To do that we can simply key path into the wrappedValue again and set: extension Binding { func transform<LocalValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { Binding<LocalValue>( get: { self.wrappedValue[keyPath: keyPath] }, set: { localValue in self.wrappedValue[keyPath: keyPath] = localValue } ) } }
— 5:43
And with this transformation we can rewrite the code that makes use of “dynamic member lookup” to just use transform instead: self.$item.transform(\.status).transform(\.isOnBackOrder)
— 6:07
In fact, this is equivalent to first appending the key paths and then transforming: self.$item.transform(\.status.isOnBackOrder)
— 6:41
Further, doing a transform with the identity key path simply does not change the binding at all: self.$item.transform(\.self)
— 7:11
Said more succinctly, the two properties we are seeing here are:
— 7:15
The transform of the appending of key paths is the appending of the transforms.
— 7:29
And, the transform of the identity is the identity.
— 7:38
We have said this pairing of phrases a ton on Point-Free, and we’ve shown that whenever such a phrase can be uttered about one of your own generic types, then there is a wonderful little world of composition hiding in plain sight.
— 7:51
The concept we are alluding to is the humble map function. We first covered the map function over 2 years ago , and in that episode we showed that map is a very universal concept that applies to many types. First of all, the Swift standard library ships with 4 maps, defined on collections, dictionaries, optionals and results, and the extended Apple ecosystem has more, such as map on Combine publishers. But there are even more map s lurking out there. We can define a notion of map on a seemingly disparate world of types, such as random number generators , parsers , asynchronous values, and more.
— 8:28
And with each of those discoveries we always had the same property: the map of a composition is the composition of the maps. For example, if we map a parser with a function f and then map the resulting parser with a function g , it’s the same as if we simply map ’d a single time with the composition of f and g together. And the exact same can be said of arrays, optionals, results, randomness, and more.
— 8:50
And what we are seeing here is something very similar, but also different in some ways. If we unroll the signature of our transform function we see it has the following shape: // (WritableKeyPath<Value, NewValue>) -> (Binding<Value>) -> Binding<NewValue>
— 9:12
Let’s also simplify the names of the generics to be a little more abstract: // (WritableKeyPath<A, B>) -> (Binding<A>) -> Binding<B>
— 9:24
That is, transform is nothing more than a way to turn key paths that go from A to B into functions that go from Binding<A> to Binding<B> . This is also pretty much exactly how we described the map function previously. It is the way to lift functions from A to B up to functions from some container of A s to a container of B s, whether it be arrays, optionals, results, or many other things: // ((A) -> B) -> ([A]) -> [B] // ((A) -> B) -> (A?) -> B? // ((A) -> B) -> (Result<A, E>) -> Result<B, E>
— 9:55
So our transform is quite similar to map in its shape and the properties it satisfies, but it’s just slightly different in that it uses a key path on the left side of the signature rather than a function.
— 10:11
This is not such a big deal though, and in fact we have done something quite similar before. We have previously seen that there is a transform known as “pullback” that expresses something quite similar to map , except it goes in the reverse direction. We saw that predicates and even snapshot testing supports this operation: // pullback: ((A) -> B) -> (Predicate<B>) -> Predicate<A> // pullback: ((A) -> B) -> (Snapshotting<B>) -> Snapshotting<A>
— 11:00
They also satisfies similar properties to map , including the pullback of the identity is the identity, and the pullback of a composition is the composition of the pullbacks.
— 11:10
But then many months after that exploration we came face-to-face with another transformation that looked eerily similar to pullback , yet was a little different. We discovered that reducers, which are the logical unit that powers the Composable Architecture, supports something that looks like a pullback , yet it uses key paths instead of functions: // pullback: (WritableKeyPath<A, B>) -> (Reducer<B>) -> Reducer<A>
— 11:33
This says that if we have a key path from A to B , then we can transform reducers on B into reducers on A , and it was the key transformation we needed to transform local reducers into global ones, which allowed us to break big problems into small ones and aided in the modularization of our applications. It also happens to satisfy all the same properties as the other pullbacks, including that the pullback of the identity key path is the identity function, and the pullback of a composition of key paths is the composition of the pullbacks.
— 12:03
We were empowered to call this pullback because for all intents and purposes it serves the same role that pullback did for predicates and snapshot testing. It turns a process that goes from A to B into a process that goes from B to A , and it satisfies some properties. It doesn’t so much matter that the process is exactly a function, it could be a key path, or many other things.
— 12:27
And that story is now playing out yet again, except this time for map . We should now be empowered to call our transform method map , for it embodies all the same principles as the map that we know and love from many other types: extension Binding { func map<NewValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { … } }
— 12:48
And then our usages would look like: self.$item.map(\.status).map(\.isOnBackOrder) self.$item.map(\.status.isOnBackOrder) self.$item.map(\.self)
— 13:07
And not only is this transformation very familiar to use because it is simply map , but it’s also exactly what “dynamic member lookup” is. The work happening in this binding transformation is exactly what “dynamic member lookup” must be doing under the hood, and in fact we could just call out to it in our implementation: extension Binding { func map<LocalValue>( _ keyPath: WritableKeyPath<Value, LocalValue> ) -> Binding<LocalValue> { self[dynamicMember: keyPath] // .init( // get: { self.wrappedValue[keyPath: keyPath] }, // set: { localValue in // self.wrappedValue[keyPath: keyPath] = localValue // } // ) } }
— 13:27
So “dynamic member lookup” is nothing but the humble map function defined on bindings. In fact, we can even go to the signature of “dynamic member lookup” for binding to see it has the exact same shape: @frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> { … public subscript<Subject>( dynamicMember keyPath: WritableKeyPath<Value, Subject> ) -> Binding<Subject> { get } }
— 13:53
And this is why we feel that this binding transformation would have been discovered by us regardless if Apple had handed it to us from on high. We’ve seen over and over that when dealing with generic types we should always be on the look out for defining operations like map because they unlock lightweight ways of transforming our types into all new types. And this is exactly what we want to do with bindings: we want to derive all new bindings from existing bindings by diving deeper into the structure the binding wraps. Bindings of optionals
— 14:22
So we are now starting to get the first real hints of why we say that SwiftUI gives us lots of tooling that is geared specifically for structs. One of its most handy transformations, the ability to deriving new bindings from existing ones, is built on the concept of “dynamic member lookup” which is intimately tied to key paths, which in turn is most used when dealing with structs and their properties. This is why we might expect to meet some resistance when trying to apply this tool to enums.
— 14:54
And we want to fix this, but let’s take a small step towards a better API. Let’s again look at one of the problematic properties we added to our Status enum, like say the quantity field: var quantity: Int { get { switch self { case let .inStock(quantity: quantity): return quantity case .outOfStock: return 0 } } set { // switch self { // case .inStock: self = .inStock(quantity: newValue) // case .outOfStock: // break // } } }
— 15:08
This is problematic for two reasons:
— 15:22
First we had to choose to return a quantity value from the outOfStock case. Of course returning 0 is perfectly reasonable, but it’d be even better to just not have to consider this case at all, after all that is why we turned to the enum in the first place.
— 15:36
Second, when setting the quantity we also had to consider what to do for the outOfStock case. Right now we have decided to flipp the state to be inStock with the specified quantity, but we could have also just done nothing. We’re not sure which is right, and again we shouldn’t have to answer questions like this because it’s the whole reason we turned to the enum to solve our domain modeling woes.
— 16:01
So, instead, let’s make it so that we don’t have to answer these questions. Let’s return an optional from this computed property so that we can properly express that sometimes we just don’t know what to do: var quantity: Int? { get { switch self { case let .inStock(quantity: quantity): return quantity case .outOfStock: return nil } } set { guard let quantity = newValue else { return } self = .inStock(quantity: quantity) } }
— 16:32
This was even something we considered earlier, but immediately ruled it out because it means the following binding is optional: self.$item.status.quantity // Binding<Int?>
— 16:38
And such a binding cannot be used with a Stepper , and so this does not compile.
— 16:46
However, now that we have seen that transforming bindings is a totally legitimate thing to do, and in fact SwiftUI comes with transformations right out the box, maybe we can now be so bold to define all new transformations.
— 16:58
The problem right now is that we have a binding of an optional Int when what we really want is a binding of an honest Int . However, we don’t always want a binding of an honest Int because it’s not possible. If the state of the item is outOfStock then we can’t possible represent this binding, and in fact we don’t want to. So maybe what we want is a transformation more of the form: // (Binding<Int?>) -> Binding<Int>?
— 17:29
Or, more generally: // (Binding<A?>) -> Binding<A>?
— 17:37
That is, it transforms a binding of an optional into an optional of a binding. And maybe a good name for such an operation could be unwrap since it’s kinda unwrapping the optional inside the binding: // unwrap: (Binding<A?>) -> Binding<A>?
— 17:45
Before even trying to implement such a function let’s understand how it could be useful.
— 17:49
If we could “unwrap” our quantity binding of the optional Int , then we’d get an optional binding of an honest Int . So if we map ’d on that optional we would get access to an honest binding of an honest Int : self.$item.status.quantity.unwrap() .map { (quantity: Binding<Int>) in … }
— 18:27
And since we have an honest binding of an honest Int we can just pass it along to the Stepper no problem: self.$item.status.quantity.unwrap().map { quantity in Section(header: Text("In stock")) { Stepper( "Quantity: \(quantity.wrappedValue)", value: quantity, in: 1...Int.max ) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } }
— 18:41
And you may not know this, but by using optional’s map here we are technically returning an optional view here, and function builders handle this situation by simply omitting this view when it is nil . So it has the same behavior as the if statement, where the view will only be rendered if quantity is non- nil .
— 18:57
And in fact in Xcode 12, which supports if - let in function builders, we will be able to do: if let quantity = self.$item.status.quantity.unwrap() { … }
— 19:16
But in the meantime map is a perfectly good way of handling this.
— 19:24
So this won’t compile until we implement this binding operator. Let’s give it a shot. We’ll first set up the signature. Since we want this operator to only work on bindings of optional, we’d like to extend Binding where it’s Value generic is optional. This isn’t super straightforward because first of all we can’t simply do this: extension Binding where Value == Optional { }
— 19:50
Because we need to specify the generic for optional: Optional<???> . We also can’t introduce a new generic to this extension so that we can pin the generic that way: extension <Wrapped> Binding where Value == Optional<Wrapped> { }
— 19:58
Hopefully someday soon Swift will support such parameterized extensions, but unfortunately it’s not yet possible.
— 20:08
Until then we can workaround this shortcoming by introducing a generic to our operator function, and then constrain the generic in that function: // extension <Wrapped> Binding where Value == Optional<Wrapped> { extension Binding { func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? { <#???#> } }
— 20:40
It’s very mechanical to do this workaround, you just move the generic from the extension to the function and the where constraint to the function, but still, it’d be really nice if we could write a parameterized extension in Swift.
— 20:51
To implement this method we need to return an optional binding. So sometimes we will return an honest binding of an honest value and sometimes we’ll return nil . The only thing we have access to is self , which is a binding of an optional, so we could start by trying to unwrap that value: extension Binding { func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? { guard if let value = self.wrappedValue else { <#???#> } <#???#> } }
— 21:12
If we fail to unwrap that value we can just return a nil binding: extension Binding { func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? { guard let value = self.wrappedValue else { return nil } <#???#> } }
— 21:17
And if we succeed then we have an honest value, which means we can return a binding that wraps that honest value: extension Binding { func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? { guard let value = self.wrappedValue else { return nil } return Binding<Wrapped>( get: { value }, set: { self.wrappedValue = $0 } ) } }
— 21:40
And there we go! Just like that our view is compiling because the unwrap operator does exactly what we needed it to do: self.$item.status.quantity.unwrap().map { quantity in Section(header: Text("In stock")) { Stepper( "Quantity: \(quantity.wrappedValue)", value: quantity, in: 1...Int.max ) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } }
— 21:58
This is pretty cool. This very simple binding transformation has allowed us to transform bindings of optionals into optional bindings of honest values, which is the perfect tool for showing or hiding a particular UI component that needs access to the binding of an honest value.
— 22:29
We can complete fixing our view by giving the same treatment to the isBackOrdered boolean. As we have said before, the property is problematic: var isBackOrdered: Bool { get { guard case let .outOfStock(isOnBackOrder) = self else { return false // return true } return isOnBackOrder } set { // switch self { // case .inStock: // break // case .outOfStock: self = .outOfStock(isOnBackOrder: newValue) // } } }
— 22:44
It is not clear what kind of boolean we should return in the case the item is in stock, and when setting this boolean on an item is in stock it isn’t clear what we should do. So, rather than trying to answer these questions and shoehorning some awkward logic into this property, let’s just refuse to answer the question by using an optional: var isBackOrdered: Bool? { get { guard case let .outOfStock(isOnBackOrder) = self else { return nil } return isOnBackOrder } set { guard let newValue = newValue else { return } self = .outOfStock(isOnBackOrder: newValue) } }
— 23:35
And now our view isn’t compiling because our binding is now of an optional, but we can simply apply the unwrap() operator to turn it into a binding of honest values and use map to transform into a view: self.$item.status.isBackOrdered.unwrap().map { isBackOrdered in Section(header: Text("Out of stock")) { Toggle(isOn: isBackOrdered) { Text("Is on back order?") } Button("Back in stock") { self.item.status = .inStock(quantity: 1) } } }
— 24:16
And now the view compiles, and if we run this in our SwiftUI preview we will see that it behaves exactly as before. The fact that the data is changing in the UI as we click around is proof that all the bindings are working correctly and that the user actions are being correctly funneled into mutations of the item we are viewing.
— 24:44
But not only is our view working the same as it did before and even the code is nice and succinct but our domain is now properly modeled with no escape hatches for accessing its data in invalid or unsafe ways. The computed properties we now have are true to the domain. If you ask an in stock item if it’s on back order it will return nil because that is a non-sensical thing to ask the item. We are no longer smudging our domain to answer questions that just don’t have a single correct answer, and by properly handling that we are making sure that anyone who uses our item is dealing with a very precisely specified data type.
— 25:22
And we are displaying the UI controls for each case of the enum in a precise manner, where as soon as the state changes to a particular case the corresponding UI control immediately shows.
— 25:31
This is starting to look really powerful. Bindings of enums
— 25:34
But this is only the beginning.
— 25:36
There are a few things that could be greatly improved in the approach we have taken so far. First of all, we needed to define optional properties on our enum for plucking out their associated data, and that is boilerplate that will be annoying to do if we have to do it for every case of every enum.
— 25:52
Second, we were motivated to explore binding transformations because we claimed that SwiftUI has a bias towards structs in that its most powerful tools work directly with key paths, and so that left our application at a disadvantage because we decided to model our state as an enum. However, the solution we came up with only works with optionals, which is an enum, but we’d like something that works with all enums, not just an enum.
— 26:17
So we want to generalize the work we have done so far. To understand what this means, let’s go back to our very first binding transformation helper: func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? { if let value = self.wrappedValue { return .init( get: { value }, set: { self.wrappedValue = $0 } ) } else { return nil } }
— 26:36
This is expressing the idea that we want to transform a binding of an optional value into an optional binding of an honest value. It allows us to safely unwrap the value inside a binding so that we can pass the binding along to UI components that only work with bindings of honest values, not optionals.
— 26:54
We want a helper like this, except it should work with any enum, not just optionals, and it should be able to isolate any case of the enum. Let’s try implementing such a generalization and see what is needed to accomplish this.
— 27:07
We’ll call the function matching , since it attempts to pattern match one of the cases inside the enum of the binding. It will be generic over the type of the case we want to extract from our binding’s value, and it will ultimately return an optional binding of an honest case value: func matching<Case>() -> Binding<Case>? { }
— 27:28
Similar to what we did for unwrap , we first need to try extracting a Case value from our binding’s value, but how can we do that? Well, whoever calls this method needs to tell us how one extracts, which is a failable function that turns Value s into Case s: func matching<Case>( extract: @escaping (Value) -> Case? ) -> Binding<Case>? { }
— 27:58
And then we can try extracting the case from the binding: func matching<Case>(extract: (Value) -> Case?) -> Binding<Case>? { guard let case = extract(self.wrappedValue) else { return nil } }
— 28:18
If we succeed, then we can construct a Binding that uses our honest, non-optional case value for the getter: func matching<Case>( extract: @escaping (Value) -> Case? ) -> Binding<Case>? { guard let case = extract(self.wrappedValue) else { return nil } return Binding<Case>( get: { case }, set: { case in } ) }
— 28:32
And technically this compiles, but it’s clearly not right because we aren’t doing anything in the set argument. This means any changes made to the binding won’t be bubbled up to our model. This closure is given an honest case value, and we need to somehow use that to overwrite our binding’s value. Sounds like the caller of this function needs to provide another piece of information to us, in particular a way to embed Case values into the Value this binding wraps: func matching<Case>( extract: @escaping (Value) -> Case?, embed: @escaping (Case) -> Value, ) -> Binding<Case>? { guard let case = extract(self.wrappedValue) else { return nil } return Binding<Case>( get: { case }, set: { case in self.wrappedValue = embed(case) } ) }
— 29:16
And now this compiles, and we could totally make use of it, but there’s something interesting here.
— 29:23
We were naturally led to introducing these extract and embed arguments to this helper because it’s exactly what we need to be able to do our job inside the function. We need a way to extract the case from the binding’s value so that we can get an honest, non-optional value to work with. And then when the binding needs to do some setting we had no choice but require we are given a way to embed case values into the binding’s value.
— 29:59
So, there’s not a lot of choice here. We’ve just kinda stumbled upon these requirements. And there’s a very good reason why these requirements seem so natural and universal, and not just choices we are forcing. It’s because the pair of extract and embed functions make up the precise way one abstractly picks apart an enum in order to isolate a particular case.
— 30:09
And in fact, this is something we explored in depth a few months ago when we introduced the concept of “ case paths ”, which are the natural companion to key paths, except they are fine tuned to work with enums rather than structs. In a nutshell, key paths are an abstraction of the concept of getters and setters for a type: struct User { var id: Int var name: String } \User.id // WriteableKeyPath<User, Int>
— 30:48
Key paths are available for every property of any type. If the property is only a getter you get a KeyPath , and if the property also has a setter then you get a WritableKeyPath . You can use the key path to get access to the property on any value: var user = User(id: 42, name: "Blob") user[keyPath: \User.id] = 100 let id = user[keyPath: \User.id]
— 31:31
Now you would never use key paths in this way because it’s far easier just to use dot syntax directly: user.id user.id = 100
— 31:43
But key paths are useful for something else besides this simple data access. They allow you to abstract over the shape of a data type by isolating a particularly property from the others so you can get and transform it without thinking about the entire structure it is embedded in.
— 32:03
Even in just this series of episodes we have already seen how key paths allowed us to abstractly transform bindings, which is essentially what “dynamic member lookup” does. Without key paths we’d have to hand over separate getter and setter functions to our transform function to get the job done.
— 32:19
Key paths are also used in quite a few Apple APIs, and in lots of open source projects, and on Point-Free we have used them to great effect for breaking large problems down into smaller ones. For example, key paths allow us to transform reducers that work on a small bit of state into reducers that work on all of app state, which allowed us to modularize our application. Transforming with case paths
— 32:40
However, what we discovered many months ago is that key paths are only half the picture. They are great for abstracting over the shape of our types, usually structs, by isolating properties, but there’s this whole other world of enums and cases. Shouldn’t there be a corresponding story that would allow us to abstractly isolate a single case from an enum so that we could write generic algorithms like we did for the key path binding transformation?
— 33:04
Well, lucky for us there is a story for enums, but not so lucky for us Swift does not provide it to us out of the box. It’s up to us to develop this tool ourselves, and that’s just what we did. We introduced a type called a CasePath , and it’s the analog of key paths, except it’s fine tuned to work with enums rather than structs.
— 33:21
In fact, we even open sourced a library that gives us access to the functionality of case paths. Let’s quickly add that library to our project.
— 33:48
Now we can import the library: import CasePaths
— 33:58
Case paths are very similar to key paths. Each case of an enum naturally leads to a case path. For example we can create a case path that focuses on the inStock case of the Status enum: let inStockCase: CasePath<Item.Status, Int>
— 34:29
To construct one of these we need to provide extract and embed functions which describe how one extracts a case from the enum, which can fail, and how to embed some data into the case of the enum. We can do it manually like so: let inStockCase = CasePath<Item.Status, Int>( embed: Item.Status.inStock(quantity:), extract: { guard case let .inStock(quantity) = status else { return nil } return quantity } )
— 35:15
Notice that the extract is exactly what we used for the get of our quantity property on Status .
— 35:19
It would be a bummer if we had to manually construct these case paths from scratch every time, just as it was a bit of a pain to construct the getter and setter properties on our Status enum. Luckily our library comes with a way to automatically generate case paths for any case of any enum, you can simply do: let inStockCase: CasePath<Item.Status, Int> = .case(Item.Status.inStock)
— 35:48
This uses Swift’s reflection capabilities to automatically derive the extract function given the embed function, and the embed function comes for free by virtue of the fact each case of an enum is a function from its associated data to the enum.
— 36:18
Further, if you have an appetite for operators, then we even provide a shorthand for this via: let inStockCase: CasePath<Item.State, Int> = /Item.Status.inStock
— 36:41
And we did this because it mimics how one generates key paths from struct properties: \User.id
— 36:54
And you can use case paths much like you would use key paths, except that allow you to extract data from an enum and embed data in an enum: let status = Item.Status.inStock(quantity: 100) let quantity = inStockCase.extract(status) // Optional(100) let newStatus = inStockCase.embed(100) // Item.State.inStock(quantity: 100)
— 37:43
But, just as with key paths, the real power comes when you write generic algorithms that allow you to use case paths to abstract over the shape of an enum. And in fact, we have just the function right here: func matching<Case>( extract: @escaping (Value) -> Case?, embed: @escaping (Case) -> Value ) -> Binding<Case>? { if let case = extract(value) { return .init( get: { case }, set: { self.wrappedValue = embed($0) } ) } else { return nil } }
— 38:02
Instead of passing the extract and embed functions in separately we can just pass in a case path: func matching<Case>( _ casePath: CasePath<Value, Case> ) -> Binding<Case>? { guard let case = casePath.extract(from: self.wrappedValue) else { return nil } return Binding<Case>( get: { case }, set: { case in self.wrappedValue = casePath.embed(case) } ) }
— 38:30
And this little helper is already powerful enough to power our UI controls: self.$item.status.matching(/Item.Status.inStock).map { quantity in Section(header: Text("In stock")) { Stepper("Quantity: \(quantity.wrappedValue)", value: quantity) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } } self.$item.status.matching(/Item.Status.outOfStock) .map { isOnBackOrder in Section(header: Text("Out of stock")) { Toggle(isOn: isOnBackOrder) { Text("Is on back order?") } Button("Back in stock") { self.item.status = .inStock(quantity: 1) } } }
— 40:46
And remember, once we can use Xcode 12 and Swift 5.3 we will be able to further simply this to just use plain if let statements: if let quantity = self.$item.status.matching(/Item.Status.inStock) { Section(header: Text("In stock")) { Stepper("Quantity: \(quantity.wrappedValue)", value: quantity) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } } if let isOnBackOrder = self.$item.status .matching(/Item.Status.outOfStock) { Section(header: Text("Out of stock")) { Toggle(isOn: isOnBackOrder) { Text("Is on back order?") } Button("Back in stock") { self.item.status = .inStock(quantity: 1) } } }
— 41:17
But most importantly, because we are using case paths, and because case paths are automatically generated for us, we get to delete all of this boilerplate since it is no longer necessary: // var isInStock: Bool { // guard case .inStock = self else { return false } // return true // } // // var quantity: Int? { // get { // switch self { // case let .inStock(quantity: quantity): // return quantity // case .outOfStock: // return nil // } // } // set { // guard let quantity = newValue else { return } // self = .inStock(quantity: quantity) // } // } // // var isBackOrdered: Bool? { // get { // guard case let .outOfStock(isOnBackOrder) = self // else { return nil /* or true?*/ } // return isOnBackOrder // } // set { // guard let newValue = newValue else { return } // self = .outOfStock(isOnBackOrder: newValue) // } // }
— 41:39
We will never have to write another property that simply emulates a getter and setter for the case of an enum because case paths give us that, and in a more ergonomic package.
— 42:13
But, we can make our new matching helper even more succinct, and fit in better with how other SwiftUI tools work. Remember that the type of binding composition that ships with Swift is powered by key paths and dynamic member lookup. What if our matching transformation could behave like that too?
— 42:33
In particular, let’s define a custom subscript on binding that allows us to optionally dive deeper into a binding’s value by using a case path: extension Binding { subscript<Case>( _ casePath: CasePath<Value, Case> ) -> Binding<Case>? { self.matching(casePath) } } This just calls matching under the hood, but maybe it should replace matching entirely.
— 43:06
And now we can express the idea of drilling down into our item’s state’s inStock case with this succinct syntax: self.$item.status[/Item.Status.inStock]
— 43:18
This transforms the binding of Item into an optional binding of an integer, representing the in-stock quantity of the item, all in one line. And we can further map on this binding to turn it into a view: self.$item.status[/Item.Status.inStock].map { quantity in Section(header: Text("In stock")) { Stepper("Quantity: \(quantity.wrappedValue)", value: quantity) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } } self.$item.status[/Item.Status.outOfStock].map { isOnBackOrder in Section(header: Text("Out of stock")) { Toggle(isOn: isOnBackOrder) { Text("Is on back order?") } Button("Back in stock") { self.item.status = .inStock(quantity: 1) } } }
— 43:44
And as soon as we have Swift 5.3 we can just use if let : if let quantity = self.$item.status[/Item.Status.inStock] { Section(header: Text("In stock")) { Stepper("Quantity: \(quantity.wrappedValue)", value: quantity) Button("Sold out") { self.item.status = .outOfStock(isOnBackOrder: false) } } } if let isOnBackOrder = self.$item.status[/Item.Status.outOfStock] { Section(header: Text("Out of stock")) { Toggle(isOn: isOnBackOrder) { Text("Is on back order?") } Button("Back in stock") { self.item.status = .inStock(quantity: 1) } } }
— 43:54
So we are now accomplishing everything that we did with the unwrap binding helper, but in a more general fashion. We can instantly abstract over any case of any enum in order to show case-specific UI controls. As soon as the state flips from one enum case to a different enum our UI will instantly update and we’ll get bindings for the data in that case so that sub views can make changes to the data and have it instantly reflected in our model.
— 44:17
This is incredibly powerful. We have truly unlocked some new functionality in SwiftUI that was previously impossible to see with the tools Apple gave us. Apple simply does not make it easy for us to use enum for state in SwiftUI, and instead all of the tools are geared towards structs.
— 44:43
But, here on Point-Free we know the importance of putting structs and enums on equal footing. Once we have a tool designed for structs we should instantly start looking for how the equivalent tool looks like for enums, and once we have a tool designed for enums we should instantly start looking for how the equivalent tool looks like for structs. And this is what led us to discover a new way to transform bindings, which also led us to a better way to construct our view hierarchy. We can now model our domain exactly as we want, and we can have the view hierarchy naturally fall out of that domain expression rather than creating a bunch of escape hatches to project information out of the domain in an imprecise manner. Next time: what’s the point?
— 45:21
But let’s push these tools even further. Let’s add a new screen to our app that shows a list of inventory as well as a new flow that allows the user to add new inventory. This sounds straightforward enough, but we will again seen that doing it can wreak havoc on our nicely modeled domain. References CasePaths Brandon Williams & Stephen Celis CasePaths is one of our open source projects for bringing the power and ergonomics of key paths to enums. https://github.com/pointfreeco/swift-case-paths Collection: Enums and Structs Brandon Williams & Stephen Celis To learn more about how enums and structs are related to each other, and to understand why we were led to define the concept of “case paths”, check out this collection of episodes: Note Enums are one of Swift’s most notable, powerful features, and as Swift developers we love them and are lucky to have them! By contrasting them with their more familiar counterpart, structs, we can learn interesting things about them, unlocking ergonomics and functionality that the Swift language could learn from. https://www.pointfree.co/collections/enums-and-structs The Many Faces of Map Brandon Williams & Stephen Celis • Apr 23, 2018 To get a better understanding of the map function and how it relates to dynamic member lookup on the Binding type, catch this early episode. Note Why does the map function appear in every programming language supporting “functional” concepts? And why does Swift have two map functions? We will answer these questions and show that map has many universal properties, and is in some sense unique. https://www.pointfree.co/episodes/ep13-the-many-faces-of-map Downloads Sample code 0108-composable-bindings-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 .