'SwiftUI DatePicker Binding optional Date, valid nil

I'm experimenting code from https://alanquatermain.me/programming/swiftui/2019-11-15-CoreData-and-bindings/

my goal is to have DatePicker bind to Binding< Date? > which allow for nil value instead of initiate to Date(); this is useful, if you have Date attribute in your core data model entity which accept nil as valid value.

Here is my swift playground code:

extension Binding {
    init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
        self.init(get: { source.wrappedValue != nil },
                  set: { source.wrappedValue = $0 ? defaultValue : nil})
    }
}

struct LiveView: View {
    @State private var testDate: Date? = nil
    var body: some View {
        VStack {
            Text("abc")

            Toggle("Has Due Date",
                   isOn: Binding(isNotNil: $testDate, defaultValue: Date()))

            if testDate != nil {
                DatePicker(
                    "Due Date",
                    selection: Binding($testDate)!,
                    displayedComponents: .date
                )
            }
        }
    }
}

let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)

I can't find solution to fix this code. It works when the toggle first toggled to on, but crash when the toggle turned back off.

The code seems to behave properly when I removed the DatePicker, and change the code to following:

extension Binding {
    init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
        self.init(get: { source.wrappedValue != nil },
                  set: { source.wrappedValue = $0 ? defaultValue : nil})
    }
}

struct LiveView: View {
    @State private var testDate: Date? = nil
    var body: some View {
        VStack {
            Text("abc")

            Toggle("Has Due Date",
                   isOn: Binding(isNotNil: $testDate, defaultValue: Date()))

            if testDate != nil {
                Text("\(testDate!)")
            }
        }
    }
}

let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)

I suspect it's something to do with this part of the code

DatePicker("Due Date", selection: Binding($testDate)!, displayedComponents: .date )

or

problem when the source.wrappedValue set back to nil (refer to Binding extension)



Solution 1:[1]

The problem is that DatePicker grabs binding and is not so fast to release it even when you remove it from view, due to Toggle action, so it crashes on force unwrap optional, which becomes nil ...

The solution for this crash is

DatePicker(
    "Due Date",
    selection: Binding<Date>(get: {self.testDate ?? Date()}, set: {self.testDate = $0}),
    displayedComponents: .date
)

backup

Solution 2:[2]

An alternative solution that I use in all my SwiftUI pickers...

I learned almost all I know about SwiftUI Bindings (with Core Data) by reading that blog by Jim Dovey. The remainder is a combination of some research and quite a few hours of making mistakes.

So when I use Jim's technique to create Extensions on SwiftUI Binding then we end up with something like this for a deselection to nil...

public extension Binding where Value: Equatable {
    init(_ source: Binding<Value>, deselectTo value: Value) {
        self.init(get: { source.wrappedValue },
                  set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
        )
    }
}

Which can then be used throughout your code like this...

Picker("Due Date", 
       selection: Binding($testDate, deselectTo: nil),
       displayedComponents: .date
) 

OR when using .pickerStyle(.segmented)

Picker("Date Format Options", // for example 
       selection: Binding($selection, deselectTo: -1)) { ... }
    .pickerStyle(.segmented)

... which sets the index of the segmented style picker to -1 as per the documentation for UISegmentedControl and selectedSegmentIndex.

The default value is noSegment (no segment selected) until the user touches a segment. Set this property to -1 to turn off the current selection.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 andrewbuilder