Reputation: 6065
(Using Xcode 12.3 and iOS 14.3)
I use a DatePicker
component with the default style (DefaultDatePickerStyle
) to display and edit a date: the component shows the current date value in a label and pops up a calendar when tapping that label. So far, so good.
When I change the date (either programmatically or manually it in the UI), the component erratically changes the date format of its label. It seems to switch between the .short
and .medium
values of DateFormatter.Style
. Note that the date format cannot be set programmatically, it's internal to DatePicker
.
Here is an example:
The DatePicker
displays "Feb 7, 2021"; I alter the date by subtracting one day using a button, causing the component to display "2/6/21"; subtracting a day again, the display changes to "Feb 5, 2021" etc. Sometimes it keeps the same format for a few dates, but mostly it toggles on every change.
Example code:
struct DateView: View {
@State var date = Date()
var body: some View {
VStack(spacing: 20) {
Button("-1 day") {
date.addTimeInterval(-24*60*60)
}
Button("+1 day") {
date.addTimeInterval(24*60*60)
}
DatePicker(
"Date",
selection: $date,
displayedComponents: .date
).labelsHidden()
}
}
}
Omitting displayedComponents
or labelsHidden
has no effect on the issue.
The same issue can be observed when repeatedly opening the calendar popup and selecting dates: after closing the popup, the displayed date sometimes is in short format, and sometimes in medium format.
Any idea what's going on there?
Upvotes: 52
Views: 7784
Reputation: 247
I experienced this problem because I had two DatePickers next to each other in an HStack.
Because the selected dates of the two pickers differed, it crossed the date boundary of where the bug happens, so the two pickers showed different formats.
I noticed that the picker responds dynamically adjusting the date format as width decreases.
So, I explicitly set the frame.width and was able to get them to be consistent.
I still used the above MyDatePicker
approach in combination with what I described above, but I did not test using the stock Apple DatePicker implementation with my own solution.
Upvotes: 0
Reputation: 10565
The accepted answer of using .id
to force the date picker to re-draw does not work consistently in my experience, and is a bit of a hack.
I've spent some time looking into this and the source of this bug appears to be with how the DatePicker
picks up the current locale, which is how it determines the date format.
I've had more success explicitly setting the locale using the environment modifier:
DatePicker().environment(\.locale, .current)
Its not perfect - I still sometimes see it briefly show a shorter date format before showing the correct format.
If you don't want to hardcode the date picker to a specific locale (even the .current
one), I recommend you add a property to your view that picks up the current locale from the environment:
@Environment(\.locale) private var locale
And then pass that in instead - this will still enable you to override the locale higher up the view hierarchy if needed.
Another thing to note about the .id
hack is it changes the behaviour of the date picker - the default behaviour in compact style is for the popover calendar to remain visible unless the user explicitly dismisses it by tapping outside once they are happy with their selection. The .id
modifier causes the picker to re-draw and the popover to close immediately.
Upvotes: 0
Reputation: 6333
I am trying to bind DatePicker
to a date on a Core Data NSManagedObject
, and I'm showing a calendar pop-up like this:
The other SwiftUI solution (recommending .id(date)
) didn't work for me, instead the popup behaved strangely and disappeared on every second tap.
What I've done instead is to put the date into a state variable:
@State private var snoozeUntilDate = Date()
And initialise this variable to the managed object's date whenever the date picker is shown:
DatePicker("Snooze until date", selection: $snoozeUntilDate, displayedComponents: .date)
.labelsHidden()
.onAppear {
snoozeUntilDate = task.snoozeUntilDate!
}
And update the managed object whenever the @State
variable changes:
.onChange(of: snoozeUntilDate) { newDate in
task.snoozeUntilDate = newDate
}
Doing this has fixed the problem for me! 🥳
While I'm not 100% sure, I think the trouble here might have been caused by the managed object. I tried making the @State
variable optional (Date?
), and that didn't re-introduce the weirdness, so it seems like the binding to the managed object (which I was creating using Binding(task.snoozeUntilDate)!
) might have been the source of the weirdness.
Upvotes: 0
Reputation: 1049
For me, the suggested workaround did work, but the date was still showing the short format for a very short moment before formatting the correct way. Another approch, which works without any issues for me, is to wrap a UIDatePicker
to use it in SwiftUI instead of using the build in view (taken from https://stackoverflow.com/a/59853272/13440564).
struct MyDatePicker: UIViewRepresentable {
@Binding var date: Date
var range: ClosedRange<Date>
func makeUIView(context: Context) -> UIDatePicker {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .date
datePicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .valueChanged)
datePicker.minimumDate = range.lowerBound
datePicker.maximumDate = range.upperBound
return datePicker
}
func updateUIView(_ datePicker: UIDatePicker, context: Context) {
datePicker.date = date
}
func makeCoordinator() -> MyDatePicker.Coordinator {
Coordinator(date: $date)
}
class Coordinator: NSObject {
private let date: Binding<Date>
init(date: Binding<Date>) {
self.date = date
}
@objc func changed(_ sender: UIDatePicker) {
self.date.wrappedValue = sender.date
}
}
}
You can the simply call it like this and customize it to your needs:
HStack {
Text("Select date")
Spacer()
MyDatePicker(
date: $date,
range: range
).frame(maxHeight: 35)
}
Upvotes: 5
Reputation: 258107
Looks like a bug. Here is a found workaround (tested with Xcode 13beta / iOS 15)
DatePicker(
"Date",
selection: $date,
displayedComponents: .date
)
.labelsHidden()
.id(date) // << here !!
Upvotes: 36