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?
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.