Search code examples
iosswiftuimultidatepicker

Select date of range between two dates in MultiDatePicker SwiftUI


I am new in SwiftUI project where I need to implement a feature using MultiDatePicker that allows a user to select two dates only. Once a date are selected, all the dates which are comes in selected date range should be automatically selected.

Here I am attaching screen shot for better understanding.

GIF file

Screenshot

In above case user select from 27th December 2024 to 31th December 2024

So is it possible where the range of date between 27th and 31th December 2024 are getting selected automatically without select manually or tap on it. Expectation like

Screenshot

Thanks in advance

Code Edited

struct count: View {
    @State private var dates: Set<DateComponents> = []
    var datesBinding: Binding<Set<DateComponents>> {
        Binding {
            return dates
        } set: { newValue in
            self.dates = newValue
        }
    }

    var body: some View {
        return VStack(spacing: 50){
            MultiDatePicker("Select dates", selection: datesBinding)
                .frame(height: 300)
        }
        .padding()
    }
 }

Here I edit the where I am trying to return the range of dates but unfortunately i have no idea how to implement the expected functionality.

Adding GIF file for better understanding

GIF file


Solution

  • One way to respond to user interaction would be to add an .onChange callback to the date picker. However, using a computed binding (as you are doing in your example) is another way.

    The advantage of using a computed binding is that changes to the set of DateComponents can be intercepted and modified, without the update causing a recursive call, as would be the case with .onChange. So using a computed binding is perhaps a better approach than using .onChange.

    Here is how you might want to intercept changes to the set of dates:

    • If the new set of dates is empty, the change can be adopted. This would be the case when the user taps on the same date twice.

    • If the size of the date set has grown larger and there are now two dates, any gaps between the dates should be filled.

    • Otherwise, the date that was just added or just removed from the set should be identified and set as a single date. This becomes the start of a new range.

    In order to convert between DateComponents and Date, it is important to use the same set of Calendar.Component as the picker is using. You will see from the post MultiDatePicker onChange not called if selection is set programmatically that this set consists of:

    [.calendar, .era, .year, .month, .day]
    

    So here is an example implementation that works as described above:

    struct ContentView: View {
        @Environment(\.calendar) var calendar
        @State private var dates: Set<DateComponents> = []
        let datePickerComponents: Set<Calendar.Component> = [.calendar, .era, .year, .month, .day]
    
        var datesBinding: Binding<Set<DateComponents>> {
            Binding {
                dates
            } set: { newValue in
                if newValue.isEmpty {
                    dates = newValue
                } else if newValue.count > dates.count {
                    if newValue.count == 1 {
                        dates = newValue
                    } else if newValue.count == 2 {
                        dates = filledRange(selectedDates: newValue)
                    } else if let firstMissingDate = newValue.subtracting(dates).first {
                        dates = [firstMissingDate]
                    } else {
                        dates = []
                    }
                } else if let firstMissingDate = dates.subtracting(newValue).first {
                    dates = [firstMissingDate]
                } else {
                    dates = []
                }
            }
        }
    
        var body: some View {
            VStack(spacing: 50){
                MultiDatePicker("Select dates", selection: datesBinding)
                    .frame(height: 300)
            }
            .padding()
        }
    
        private func filledRange(selectedDates: Set<DateComponents>) -> Set<DateComponents> {
            let allDates = selectedDates.compactMap { calendar.date(from: $0) }
            let sortedDates = allDates.sorted()
            var datesToAdd = [DateComponents]()
            if let first = sortedDates.first, let last = sortedDates.last {
                var date = first
                while date < last {
                    if let nextDate = calendar.date(byAdding: .day, value: 1, to: date) {
                        if !sortedDates.contains(nextDate) {
                            let dateComponents = calendar.dateComponents(datePickerComponents, from: nextDate)
                            datesToAdd.append(dateComponents)
                        }
                        date = nextDate
                    } else {
                        break
                    }
                }
            }
            return selectedDates.union(datesToAdd)
        }
    }
    

    In your animated gif, the range of dates was shown with a solid background. I think you would need to implement your own custom date picker to achieve this effect, the native MultiDatePicker always shows the selection as a set of individual dates.

    Animation