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):
DatePicker()
in my app:
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.