Home > database >  MultiDatePicker onChange not called if selection is set programmatically
MultiDatePicker onChange not called if selection is set programmatically

Time:12-18

I try to select a date (today) in a MultiDatePicker programmatically via a Button. The selection works as expected and the onChange modifier will be called and the day today is marked as selected in the Picker. But when I try to deselect the date directly in the MultiDatePicker, the date is not marked anymore but the onChange modifier will not get called. If you tap on another date in the MultiDatePicker now, both dates, the other date and the date today are marked as selected.

import SwiftUI

struct ContentView: View {

    @Environment(\.calendar) private var calendar

    @State private var selectedDates: Set<DateComponents> = []

    @State private var onChangeCounter = 0
    
    var body: some View {
        VStack {
            MultiDatePicker("Select dates", selection: $selectedDates)
                .frame(height: UIScreen.main.bounds.width)
                .onChange(of: selectedDates) { _ in
                    self.onChangeCounter  = 1
                }

            Button("Select today") {
                let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
                selectedDates.insert(todayDatecomponents)
            }
            .foregroundColor(.white)
            .frame(minWidth: 150)
            .padding()
            .background(Color.accentColor)
            .cornerRadius(20)

            HStack {
                Text("onChangeCounter")
                Spacer()
                Text(String(onChangeCounter))
            }
            .padding()

            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

iPhone Screenvideo

Am I doing something wrong or is it just not possible to select a date in the MultiDatePicker programmatically?

Thank You!

CodePudding user response:

For purposes of this discussion, Today means December 17, 2022

The issue is that Date.now is not equal to Today

I'm in US East Coast Time Zone... if I add a button to print(Date.now) and tap it, I see this in the debug console:

2022-12-17 14:08:52  0000

if I tap it again 4-seconds later, I see this:

2022-12-17 14:08:56  0000

Those two dates are not equal.

So, let's find out what the MultiDatePicker is using for it's selection.

Change your MultiDatePicker to this:

        MultiDatePicker("Select dates", selection: $selectedDates)
            .frame(height: UIScreen.main.bounds.width)
            .onChange(of: selectedDates) { _ in
                print("onChange")
                selectedDates.forEach { d in
                    print(d)
                }
                print()
                self.onChangeCounter  = 1
            }

If I run the app and select Dec 14, 19 and 8, I see this:

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 19 isLeapMonth: false 

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 19 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false 

Now, I de-select the 19th, and I see this:

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false 

The 19th was correctly removed from the Set.

Now, I tap your "Select today" button, and I see this:

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false 
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 24 second: 11 nanosecond: 339460015 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false 

As we can see, these two lines:

let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
selectedDates.insert(todayDatecomponents)

Insert a DateComponents object with a lot more detail.

If I tap "Select today" again, I get this:

onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false 
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false 
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 24 second: 11 nanosecond: 339460015 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false 
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 26 second: 39 nanosecond: 866878032 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false 

Now selectedDates contains two "today dates" ... 2-minutes and 28-seconds apart.

When I tap the 17th on the calendar, there is no matching date in that set to remove... so when the calendar refreshes (such as when I select another date), the 17th still shows as selected.

So, let's change the programmatically inserted DateComponents to match the calendar's data:

let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: Date.now)
selectedDates.insert(todayDatecomponents)

Now when we tap 17 on the calendar it will be de-selected and the matching object selectedDates will be removed.

Here's how I modified your code to debug:

import SwiftUI

@available(iOS 16.0, *)
struct MultiDateView: View {
    @Environment(\.calendar) private var calendar
    
    @State private var selectedDates: Set<DateComponents> = []
    
    @State private var onChangeCounter = 0
    
    var body: some View {
        VStack {
            MultiDatePicker("Select dates", selection: $selectedDates)
                .frame(height: UIScreen.main.bounds.width)
                .onChange(of: selectedDates) { _ in
                    print("onChange")
                    selectedDates.forEach { d in
                        print(d)
                    }
                    print()
                    self.onChangeCounter  = 1
                }
            
            Button("Select today") {
                let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
                selectedDates.insert(todayDatecomponents)
            }
            .foregroundColor(.white)
            .frame(minWidth: 150)
            .padding()
            .background(Color.accentColor)
            .cornerRadius(20)
            
            Button("Select today the right way") {
                let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: Date.now)
                selectedDates.insert(todayDatecomponents)
            }
            .foregroundColor(.white)
            .frame(minWidth: 150)
            .padding()
            .background(Color.green)
            .cornerRadius(20)
            
            HStack {
                Text("onChangeCounter")
                Spacer()
                Text(String(onChangeCounter))
            }
            .padding()
            
            Button("Print Date.now in debug console") {
                print("debug")
                print("debug:", Date.now)
                print()
            }
            .foregroundColor(.white)
            .frame(minWidth: 150)
            .padding()
            .background(Color.red)
            .cornerRadius(20)
            
            Spacer()
        }
    }
}

@available(iOS 16.0, *)
struct MultiDateView_Previews: PreviewProvider {
    static var previews: some View {
        MultiDateView()
    }
}

CodePudding user response:

it looks like MultiDatePicker is saving additional components.

Use this to set the today components:

let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: .now)

  • Related