swiftuiswiftui-datepicker

Why is the DatePicker not displaying in the confirmationDialog?


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()


Solution

  • 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.