This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We explore different ways to create a binding to an optional value.

00:06 After 10 episodes about the attribute graph, it's time to talk about some more practical things again. We deserve it.

00:20 We want to play around with binding transformations while building a component that lets you pick a time from a few static options — e.g. "tonight" or "tomorrow" — or a custom date. Only when the custom date option is selected do we want to show a date picker.

01:20 Let's start by writing an enum that models the component's state:

enum Time {
    case tonight
    case tomorrow
    case custom(Date)
}

02:33 In the picker's body, we place three buttons to switch between the enum cases, as well as a text view displaying the current selection. We aren't worrying too much about the visual design of the picker, as long as it works functionally:

struct TimePicker: View {
    @State private var selection = Time.tonight

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            Text("\(selection)")
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            TimePicker()
        }
        .padding()
        .dynamicTypeSize(.xxxLarge)
    }
}

03:54 We can now tap the buttons to switch between the enum cases. Next, we want to show a DatePicker if the custom date is selected. This picker takes a binding to a Date, so we have to find a way to derive a binding from the associated value in the .custom case.

Manual Binding

04:30 The first option we can try is to create a binding manually by providing getter and setter closures. We first match the selection's case to .custom, and we bind the associated value to a local variable. Then we create a binding with a getter that returns the variable, and a setter to wrap a new date in a .custom case:

struct TimePicker: View {
    @State private var selection = Time.tonight
    
    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case let .custom(date) = selection {
                let binding = Binding {
                    date
                } set: {
                    selection = .custom($0)
                }
                DatePicker("Custom Date", selection: binding)
            }
            Text("\(selection)")
        }
    }
}

06:03 This works: we're able to switch to a custom date, we can change the date, and the text view displays the picked date. However, there's a big downside to using manually constructed bindings. By creating a binding from getter and setter closures, we prevent SwiftUI from checking if the binding's value has actually changed and whether or not the view using the binding needs to be rerendered, thereby forcing rerenders with every update.

07:58 We want to visualize this effect, but we can't look into the DatePicker, because we don't own it. But there's a handy little trick we can use; we can wrap the picker in another helper view and give it a random background color. That way, every time the view is rerendered, we'll see a different color. Or, more importantly, if we see the same color, we'll know that the view wasn't redrawn:

struct Helper: View {
    @Binding var date: Date

    var body: some View {
        DatePicker("Custom Date", selection: $date)
            .background(Color(hue: .random(in: 0...1), saturation: 1, brightness: 1, opacity: 0.3))
    }
}

09:39 We replace the picker with a Helper view, passing in the manually constructed binding. And we need to mutate some other piece of state to demonstrate that the helper view gets reexecuted, so we also add a Boolean flag and a toggle:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case let .custom(date) = selection {
                let binding = Binding {
                    date
                } set: {
                    selection = .custom($0)
                }
                Helper(date: binding)
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

10:49 Because the body of TimePicker depends on the flag state, it's reexecuted every time we flip the toggle. And as we can see, the nested Helper view also gets reexecuted. None of its dependencies actually change, but SwiftUI doesn't know that.

11:17 If we pass in a .constant binding, the picker's background color doesn't change when we flip the toggle, because now SwiftUI detects that the binding's value is unchanged:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case let .custom(date) = selection {
//                let binding = Binding {
//                    date
//                } set: {
//                    selection = .custom($0)
//                }
                Helper(date: .constant(date))
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

11:41 Creating a binding from a state variable also works fine. SwiftUI knows it doesn't have to redraw the Helper view when we flip the toggle, because it can compare the date binding's values:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false
    @State private var date = Date.now

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case let .custom(_) = selection {
//                let binding = Binding {
//                    date
//                } set: {
//                    selection = .custom($0)
//                }
                Helper(date: $date)
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

12:23 In a lot of cases, this difference in efficiency won't make a big difference, but as a general practice, it'd be good to avoid creating bindings manually. It's better to create a binding from a state property directly, with or without using a key path, because then SwiftUI can compare the binding's values.

Binding from a Key Path

13:07 Let's try to create our binding with a key path. Point-Free created the CasePaths library for this, but we can also type out a computed property of an optional Date? on Time. In the getter, we return the custom date if the selection is set to the .custom case. In the setter, we check if the new value is non-nil, and if it is, we wrap it in a Time.custom:

extension Time {
    var customDate: Date? {
        get {
            guard case .custom(let date) = self else {
                return nil
            }
            return date
        }
        set {
            if let newValue {
                self = .custom(newValue)
            }
        }
    }
}

14:13 This implementation isn't perfect, because it's unclear what should happen if the optional property is being set to nil. Currently, we just ignore this case.

14:25 Now we can use a key path to construct a binding to the optional customDate of the selection and pass that to a special initializer of Binding. This initializer takes a binding to an optional value, and it transforms it into an optional binding to a non-optional value. Finally, we use if let to unwrap this optional binding and pass it to our DatePicker:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if let binding = Binding($selection.customDate) {
                Helper(date: binding)
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

15:59 It seems to work; we can change the custom date, and the date picker doesn't unnecessarily redraw when we flip the toggle.

16:39 But if we switch the selection to .tomorrow, we get a crash. We've examined this, but we don't really understand why the crash happens. We aren't force-unwrapping any optional values, which we can make even clearer by wrapping the date picker in another if-case statement:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case .custom = selection {
                if let binding = Binding($selection.customDate) {
                    Helper(date: binding)
                }
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

17:20 We're now extra sure the Helper view with the nested DatePicker is only added to the view if the custom date is selected, but the app still crashes when we switch from a custom date to a static option. The DatePicker seems to access the binding when it shouldn't.

Binding to a Non-Optional Value

19:31 Perhaps we can prevent the crash from happening by using a default value instead of trying to unwrap an optional. We write another computed property that doesn't return a nil if we're in a different case than .custom, but it returns the current date by default:

extension Time {
    var customDate: Date? {
        // ...
    }

    var customDateOrDefault: Date {
        get {
            guard case let .custom(date) = self else {
                return Date.now
            }
            return date
        }
        set {
            self = .custom(newValue)
        }
    }
}

21:02 Now we can create a binding from the selection state by appending the key path to the customDateOrDefault property:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false
    @State private var date = Date.now

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case .custom = selection {
                Helper(date: $selection.customDateOrDefault)
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

21:33 We still keep the outer if-case statement to make sure we only show the date picker if the custom date option is selected, but we're able to remove the if-let statement because we're dealing with a non-optional date.

21:48 This circumvents the issue because we're able to switch between the various options and change the custom date. Flipping the toggle also shows that the date picker isn't redrawn when it doesn't have to be.

Subscript

22:10 If we want to provide our own default date value, we can use a subscript to construct a binding to a non-optional value. First, we add a subscript to the Time struct:

extension Time {
    var customDate: Date? {
        // ...
    }

    var customDateOrDefault: Date {
        // ...
    }

    subscript(customDateOrDefault date: Date) -> Date {
        get {
            guard case let .custom(d) = self else { return date }
            return d
        }
        set {
            self = .custom(newValue)
        }

    }
}

22:51 And then we construct our binding using the subscript, passing in any default date we want:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case .custom = selection {
                Helper(date: $selection[customDateOrDefault: Date.now])
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

23:14 We can refactor this to work for any type of optional value, and not just custom dates, by writing an extension of Optional:

extension Optional {
    subscript(orDefault value: Wrapped) -> Wrapped {
        get {
            self ?? value
        }
        set {
            self = newValue
        }
    }
}

24:18 Now we use the generic subscript on Optional to provide our default value:

struct TimePicker: View {
    @State private var selection = Time.tonight
    @State private var flag = false

    var body: some View {
        VStack {
            Button("Tonight") { selection = .tonight }
            Button("Tomorrow") { selection = .tomorrow }
            Button("Custom") { selection = .custom(Date.now) }
            if case .custom = selection {
                Helper(date: $selection.customDate[orDefault: Date.now])
            }
            Toggle("Toggle", isOn: $flag)
            Text("\(selection)")
        }
    }
}

24:37 Unfortunately, these approaches where we use a subscript have the same issue as when we manually constructed our binding with getter and setter closures — the date picker gets redrawn when we flip the toggle. Apparently, SwiftUI doesn't compare the values of a binding that uses a subscript, and it always rerenders a view that uses this type of binding.

26:19 So, the only way to avoid an unnecessary view update is by using the computed property that returns a default value. The binding created with a subscript works, but it has the same issue as the manually constructed binding. Clearly, bindings can be pretty tricky to get right.

26:40 Something to look at next time is finding a way to preserve a previously picked custom date when we switch to one of the predefined options, so that when we go back to the custom date option, that custom value is still there. For this, we might have to change the enum or store another piece of state.

References

Resources

  • Sample Code

    Written in Swift 6.0

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

172 Episodes · 60h14min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes