binding-with-casepath

Advanced Techniques with Binding: Transforming and Adapting State

While the standard property wrappers handle most state management needs, you will often encounter situations where the shape of your state doesn’t perfectly match the requirements of a SwiftUI view. For example, a view might need a Binding<String>, but your model provides a Binding<String?>. Or a view needs to bind to the associated value of an enum case.

SwiftUI’s Binding type is incredibly powerful, and with a few extension methods, we can transform bindings to fit our exact needs. Let’s use the following data model for our examples:

1
2
3
4
5
6
7
8
9
10
struct Item: Hashable, Identifiable {
let id = UUID()
var color: Color?
var status: Status

enum Status: Hashable {
case inStock(quantity: Int)
case outOfStock(isOnBackOrder: Bool)
}
}

Given a state variable like @State var item: Item, a binding to its status, $item.status, would have the type Binding<Item.Status>. Let’s see how we can manipulate this and other bindings.

Drilling Down into Bindings with Key Paths

SwiftUI has built-in support for creating bindings to the properties of a bound value. When you write $item.status, Swift is transparently applying a transform to map the Binding<Item> to a Binding<Item.Status>. This is conceptually achieved through a map function that uses a WritableKeyPath:

1
2
3
4
5
extension Binding {
func map<LocalValue>(_ keyPath: WritableKeyPath<Value, LocalValue>) -> Binding<LocalValue> {
self[dynamicMember: keyPath]
}
}

You rarely need to call this map function directly, as the . syntax ($item.status) is convenient shorthand for the same operation. It’s the simplest way to transform a Binding<A> into a Binding<B>.

Handling Optionals: The unwrap Extension

A very common scenario is dealing with optional state. For instance, our item.color property is a Color?, making $item.color a Binding<Color?>. However, SwiftUI’s ColorPicker view requires a Binding<Color>. It cannot work with optionals.

To bridge this gap, we can create a handy unwrap extension that transforms an optional binding Binding<A?> into a non-optional binding Binding<A>?. The result is itself an optional: it will be nil if the original state is nil, or a valid, non-optional Binding if the state has a value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Binding {
/// Transforms a `Binding<Wrapped?>` into a `Binding<Wrapped>?`.
/// If the original binding's value is `nil`, this returns `nil`.
func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? {
// If the wrapped value is nil, we can't create a binding.
guard let value = self.wrappedValue else { return nil }

// Create a new binding that gets the non-optional value
// and sets the optional value on the original source.
return Binding<Wrapped>(
get: { value },
set: { self.wrappedValue = $0 }
)
}
}

Usage:

You can use this with an if let statement to conditionally show a view that requires a non-optional binding.

1
2
3
4
5
6
7
8
9
10
11
12
struct ItemEditorView: View {
@Binding var item: Item

var body: some View {
// Only show the ColorPicker if a binding to a non-optional Color can be created.
if let colorBinding = $item.color.unwrap() {
ColorPicker("Item Color", selection: colorBinding)
} else {
Text("This item has no color set.")
}
}
}

A More General Solution for Enums: matching with CasePaths

The unwrap function is actually a specific version of a broader problem: how do we bind to the associated value of an enum case? An Optional is just an enum with two cases: .none and .some(Wrapped). unwrap effectively extracts the associated value from the .some case.

To create a more generic solution for any enum, we can leverage the excellent CasePaths library. A CasePath is like a “key path for an enum case,” allowing you to reliably extract associated values from and embed them back into an enum.

Building on this, we can create a matching function that returns a binding to an associated value if and only if the binding’s value is currently in that specific case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Requires the 'CasePaths' library: https://github.com/pointfreeco/swift-case-paths
import CasePaths

extension Binding {
/// Returns a new binding focused on a specific case of an enum.
/// If the original binding's value does not match the case, this returns `nil`.
func matching<Case>(
_ casePath: CasePath<Value, Case>
) -> Binding<Case>? {
// Attempt to extract the associated value from the current state.
guard let `case` = casePath.extract(from: self.wrappedValue) else { return nil }

// Create a new binding.
return Binding<Case>(
// The getter returns the extracted associated value.
get: { `case` },
// The setter embeds the new value back into the case and updates the original source.
set: { `case` in self.wrappedValue = casePath.embed(`case`) }
)
}
}

Usage:

This powerful extension allows you to build UI that adapts to the state of an enum. For our Item.Status, we can show completely different controls for each case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ItemStatusView: View {
@Binding var status: Item.Status

var body: some View {
VStack {
// This UI will only appear and work if status is .inStock
if let quantityBinding = $status.matching(/Item.Status.inStock) {
Stepper("Quantity: \(quantityBinding.wrappedValue)", value: quantityBinding)
}

// This UI will only appear and work if status is .outOfStock
if let onBackOrderBinding = $status.matching(/Item.Status.outOfStock) {
Toggle("Is on backorder", isOn: onBackOrderBinding)
}
}
}
}

These techniques for transforming Binding are essential for writing clean, decoupled SwiftUI code. They allow your views to remain simple and focused on their specific data requirements, while your data models can remain complex and robust.