Why is the DatePicker not displaying in the confirmationDialog?

185 Views Asked by At

I'm trying to put a DatePicker() in a .confirmationDialog, but it's not displaying. My overall goal is to try and replicate the Journal app (in iOS 17.2 beta), so I need this look at least. I'm completely stuck, so even a vague pointer or idea might help me get to the solution.

import SwiftUI

struct DatePickerInConfirmationDialog: View {
    @State var showDialog = false
    @State var date = Date.now
    var body: some View {
        Button("Open dialog") {showDialog = true}
            .confirmationDialog("Set Custom Date", isPresented: $showDialog) {
                DatePicker("Enter date", selection: $date)
                    .datePickerStyle(.compact)
                    .frame(height: 400)
            } message: {
                Text("Set Date")
            }
    }
}

#Preview {
    DatePickerInConfirmationDialog()
}

DatePicker() in the 'Journal' App (iOS 17.2 beta): .comfirmationDialog with DatePicker()

DatePicker() in my app: .comfirmationDialog missing DatePicker()

1

There are 1 best solutions below

0
On BEST ANSWER

Here is an existing answer for how to show a UIDatePicker in an action sheet, in UIKit.

Note that a confirmationDialog in SwiftUI doesn't necessarily show an action sheet. I think it could be presented as just an alert depending on size classes, so you might have to check the size classes if you want to 100% replicate the same behaviour.

The solution adds a view to UIAlertController.view, which the documentation tells you explicitly not to do. There likely isn't a "proper" way to do this because of that, unless Apple adds dedicated APIs to do this in future iOS versions.

We can port it to SwiftUI by using a hidden (zero-sized) UIViewControllerRepresentable to present the UIAlertController.

struct DatePickerActionSheet: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    
    
    let doneClicked: ((Date) -> Void)?
    let cancelClicked: (() -> Void)?
    
    func makeUIViewController(context: Context) -> UIViewController {
        context.coordinator.vc
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        context.coordinator.doneClicked = {
            isPresented = false
            doneClicked?($0)
        }
        context.coordinator.cancelClicked = {
            isPresented = false
            cancelClicked?()
        }
        if isPresented {
            context.coordinator.present()
        }
    }
    
    func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIViewController, context: Context) -> CGSize? {
        .zero
    }
    
    @MainActor
    class Coordinator {
        let vc = UIViewController()
        
        var doneClicked: ((Date) -> Void)?
        var cancelClicked: (() -> Void)?
        
        func present() {
            let dateChooserAlert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
            let datePicker = UIDatePicker()
            datePicker.preferredDatePickerStyle = .inline
            datePicker.datePickerMode = .date
            datePicker.translatesAutoresizingMaskIntoConstraints = false
            dateChooserAlert.view.addSubview(datePicker)
            dateChooserAlert.addAction(UIAlertAction(title: "Done", style: .default, handler: { action in
                    self.doneClicked?(datePicker.date)
                }))
            
            dateChooserAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { action in
                    self.cancelClicked?()
                }))
            NSLayoutConstraint.activate([
                datePicker.leftAnchor.constraint(equalTo: dateChooserAlert.view.leftAnchor),
                datePicker.rightAnchor.constraint(equalTo: dateChooserAlert.view.rightAnchor),
                datePicker.topAnchor.constraint(equalTo: dateChooserAlert.view.topAnchor),
                // You'd need to adjust this 500 constant according to how many buttons you have
                dateChooserAlert.view.heightAnchor.constraint(equalToConstant: 500)
            ])
            
            vc.present(dateChooserAlert, animated: true, completion: nil)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
}

Usage:

@State var isPresented = false

var body: some View {
    VStack {
        Button("Select Date") {
            isPresented = true
        }
        DatePickerActionSheet(isPresented: $isPresented) {
            print($0)
        } cancelClicked: {
            
        }
    }
}

Also consider adding a @Binding for the selected date, instead of getting it from the doneClicked closure.