iosswiftswiftuidatepickermultidatepicker

Is there any way to select entire week in MultiDatePicker?


I'm working on a SwiftUI project where I need to implement a feature using MultiDatePicker that allows a user to select an entire week by tapping on a single date within that week. Once a date is selected, the whole week it belongs to should be automatically selected. If a user selects a date from a different week, the previously selected week should be deselected, and the new week should be selected instead.

import SwiftUI

struct ContentView: View {
    @State private var dates: Set<DateComponents> = []

    var body: some View {
        MultiDatePicker("", selection: $dates)
            .frame(height: 300)
            .onChange(of: dates) { oldValue, newValue in
                if let selectedDate = newValue.first, let date = Calendar.current.date(from: selectedDate) {
                    selectWeek(for: date)
                }
            }
    }

    private func selectWeek(for date: Date) {
        let calendar = Calendar.current
        let weekBoundary = calendar.weekBoundary(for: date) // this function calculates the whole week
        for dayOffset in 0..<7 {
            if let dateOfDay = calendar.date(byAdding: .day, value: dayOffset, to: weekBoundary.startOfWeek) {
                let components = calendar.dateComponents([.year, .month, .day], from: dateOfDay)
                dates.insert(components)
            }
        }
    }
}

This is the desired output: enter image description here

The problem is that the selection just works 1 time (1st time).

Has anyone successfully implemented a similar week-selection feature in SwiftUI? If so, how did you approach it? Are there alternative components or custom implementations that could facilitate selecting an entire week more efficiently than using MultiDatePicker?


Solution

  • When you select a different week in the picker, the dates selected previously will not be automatically removed, so just using dates.first to find the newly selected date is incorrect.

    You would need to find the difference between the new value and old value of dates - then you know which new date is selected. If a date is deselected, presumably you want to deselect the whole week, I think it would be better to put this logic in a custom Binding:

    var datesBinding: Binding<Set<DateComponents>> {
        Binding {
            return dates
        } set: { newValue in
            let added = newValue.subtracting(dates) // find the difference
            // if a new date selected, "added" should contain a single element
            if let firstAdded = added.first, let date = calendar.date(from: firstAdded) {
                selectWeek(for: date)
            } else { // this means a date is deselected, so deselect the whole week
                dates = []
            }
        }
    }
    
    @Environment(\.calendar) var calendar
    
    var body: some View {
        MultiDatePicker("", selection: datesBinding)
            .frame(height: 300)
    }
    

    Note that in selectWeek you should first set dates = [], otherwise you would not be deselecting the previously selected week.