20

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)

5 Answers 5

26

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
)
Sign up to request clarification or add additional context in comments.

2 Comments

Your solution fixed the crash but is not behaving as it intended, because we want the DatePicker to modify the value of testDate. In your code, the DatePicker selection bind to .constant(Date()) instead of testDate when the toggle on. However, I think you're right that the crash due to force unwrap nil optional, seems that when the toggle turned off DatePicker still trying to unwrap optional(which now set to nil), I expect the code not doing that since I already put a check if testdate != nil which incorrect. Still looking for solution to resolve this.
Updated with another non-crashable variant
6

Most optional binding problems can be solved with this:

public func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

Here's how I use it with DatePicker:

DatePicker(
    "",
    selection: $testDate ?? Date(),
    displayedComponents: [.date]
)

1 Comment

Nice binding hack! Thanks!
4

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.

Comments

4

To solve this issue, you create an extension on Binding to automatically provide with an easy way to use optionals. Place this where your other global extensions exist in your project.

extension Binding {
    func bindUnwrap<T>(defaultVal: T) -> Binding<T> where Value == T? {
        Binding<T>(
            get: {
                self.wrappedValue ?? defaultVal
            }, set: {
                self.wrappedValue = $0
            }
        )
    }
}

Then, to use that extension with a DatePicker in a view:

DatePicker(
    "My Title",
    selection: $myVar.bindUnwrap(defaultVal: Date()),
    in: ...Date(),
    displayedComponents: [.date]
)

Hope that helps!

Comments

2

Here is my solution, I added a button to remove the date and add a default date. All it's wrapped in a new component

https://gist.github.com/Fiser12/62ef54ba0048e5b62cf2f2a61f279492

import SwiftUI

struct NullableBindedValue<T>: View {
    var value: Binding<T?>
    var defaultView: (Binding<T>, @escaping (T?) -> Void) -> AnyView
    var nullView: ( @escaping (T?) -> Void) -> AnyView

    init(
        _ value: Binding<T?>,
        defaultView: @escaping (Binding<T>, @escaping (T?) -> Void) -> AnyView,
        nullView: @escaping ( @escaping (T?) -> Void) -> AnyView
    ) {
        self.value = value
        self.defaultView = defaultView
        self.nullView = nullView
    }

    func setValue(newValue: T?) {
        self.value.wrappedValue = newValue
    }

    var body: some View {
        HStack(spacing: 0) {
            if value.unwrap() != nil {
                defaultView(value.unwrap()!, setValue)
            } else {
                nullView(setValue)
            }
        }
    }
}

struct DatePickerNullable: View {
    var title: String
    var selected: Binding<Date?>
    @State var defaultToday: Bool = false

    var body: some View {
        NullableBindedValue(
            selected,
            defaultView: { date, setDate in
                let setDateNil = {
                    setDate(nil)
                    self.defaultToday = false
                }

                return AnyView(
                    HStack {
                        DatePicker(
                            "",
                            selection: date,
                            displayedComponents: [.date, .hourAndMinute]
                        ).font(.title2)
                        Button(action: setDateNil) {
                            Image(systemName: "xmark.circle")
                                .foregroundColor(Color.defaultColor)
                                .font(.title2)
                        }
                        .buttonStyle(PlainButtonStyle())
                        .background(Color.clear)
                        .cornerRadius(10)
                    }
                )
            },
            nullView: { setDate in
                let setDateNow = {
                    setDate(Date())
                }

                return AnyView(
                    HStack {
                        TextField(
                            title,
                            text: .constant("Is empty")
                        ).font(.title2).disabled(true).textFieldStyle(RoundedBorderTextFieldStyle())
                        Button(action: setDateNow) {
                            Image(systemName: "plus.circle")
                                .foregroundColor(Color.defaultColor)
                                .font(.title2)
                        }
                        .buttonStyle(PlainButtonStyle())
                        .background(Color.clear)
                        .cornerRadius(10)
                    }.onAppear(perform: {
                        if self.defaultToday {
                            setDateNow()
                        }
                    })
                )
            }
        )
    }
}

4 Comments

I think it's best to avoid the if in the body of the NullableBindedValue View
Nice solution! Could maybe be more readable if typealias was used for the lengthy variable signatures?
Yes I agree, the typealias would good to improve readability. Why you thing that would be best to avoid the if?, it's necessary. Another stuff that could be improve is to remove the AnyView, for example with 2 generic views
I used the version provided via gist in the answer. Thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.