0

I'm trying to build a DatePicker that can handle an optional date.

I've used this post as a jumping off point... but my code, at present, crashes after just a couple of toggles between nil and Date().

How can I make this code safe?

import SwiftUI

extension Date {
    public var m3d2y4: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM dd, yyyy"
        return formatter.string(from: self)
    }
}

struct OptionalDatePicker: View {
    @Binding var date: Date?
    var nilText = "Never"
    var nilDate = Date()
    
    var body: some View {
        if let _ = date {
            datePicker
        } else {
            nilTextButton
        }
    }
    
    private var dateBinding: Binding<Date> {
        Binding(get: { date ?? nilDate }, set: { date = $0 })
    }
    
    private var dateString: String {
        dateBinding.wrappedValue.m3d2y4
    }

    private var nilTextButton: some View {
        Button(action: toggleDate) {
            Text(nilText)
        }
    }
    
    private var datePicker: some View {
        ZStack {
            HStack {
                Text(dateString)
                    .foregroundColor(.blue)
                Button(action: { toggleDate() }) {
                    Image(systemName: "xmark.circle")
                }
            }
            DatePicker("", selection: dateBinding, displayedComponents: [.date])
                .opacity(0.02) // minimum opacity to still allow tapping
                .labelsHidden()
        }
    }
    
    private func toggleDate() {
        if let _ = date {
            date = nil
        } else {
            date = nilDate
        }
    }
}


struct OptionalDatePicker_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
    
    struct ContentView: View {
        @State var date: Date? = nil
        
        var body: some View {
            HStack {
                Text("Ends")
                Spacer()
                OptionalDatePicker(date: $date)
            }
            .padding()
        }
    }
}

Thanks in advance!

emehex
  • 9,874
  • 10
  • 54
  • 100
  • You do something weird here - `.opacity(0.02)` is a reason. – Asperi Sep 02 '21 at 15:09
  • The Picker is in front of the Text label so that I can still bring up the calendar on tap. If `opacity` set to `0`, it's no longer tappable (not sure why?)... With some trial-and-error I've determined that `.opacity(0.02)` is the lowest value that still allows and captures a tap. If `DatePicker` had an `isPresented` argument like `NavigationLink` I don't think I'd need to do it this way. – emehex Sep 02 '21 at 16:03

1 Answers1

0

Noting the SO post you link to in your question, I'll repeat my answer here...

I learned almost all I know about SwiftUI Bindings (with Core Data) by reading this 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("", 
       selection: Binding($date, deselectTo: nil),
       displayedComponents: [.date]
) 

... where date is an optional, for example...

@State private var date: Date?
andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
  • In your example you are using `Picker` instead of `DatePicker`. If you try to use that binding on a `DatePicker` you get: `Cannot convert value of type 'Binding' to expected argument type 'Binding'` – Lluis Gerard Apr 13 '22 at 08:06
  • @LluisGerard thanks for pointing this out... I'll do some testing, but on first look, the error message you've presented is a type mismatch, so perhaps that is unrelated to the type of picker that is being used? I'll have a closer look in the next day or so. – andrewbuilder Apr 13 '22 at 14:20