-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor calendar to support dynamic selection type changes #2130
Changes from all commits
1730bd1
622ef5d
e34538c
d2560ee
26a38d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,111 +18,11 @@ | |
|
||
import SwiftUI | ||
|
||
struct CalendarSelectableCell: View { | ||
let selectionType: CalendarSelectionType | ||
let calendar: Calendar | ||
let accessibilityProvider: CalendarAccessibilityConfiguration | ||
let dayDate: Date | ||
let onSelection: (Date) -> Void | ||
struct CalendarSelectableCell<Cell: View>: View { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nothing to see here, just reverting back to the old implementation, which supports better composition of views among other things |
||
@ViewBuilder let cell: Cell | ||
let onSelection: () -> Void | ||
|
||
var body: some View { | ||
Group { | ||
switch selectionType { | ||
case .range(let selectionState, _): | ||
rangeView(selectionState: selectionState.wrappedValue) | ||
case .single(let selection, _): | ||
singleView(selectionState: selection.wrappedValue) | ||
} | ||
}.onTapGesture(perform: { | ||
onSelection(dayDate) | ||
}) | ||
} | ||
|
||
@ViewBuilder private func rangeView(selectionState: CalendarRangeSelectionState?) -> some View { | ||
if case .intermediate(let date) = selectionState, | ||
initialSelection(date, matchesDate: dayDate) { | ||
singleCell(date: date) | ||
} else if case .range(let range) = selectionState, range.contains(dayDate) { | ||
rangeCell(closedRange: range, highlightRangeEnds: true) | ||
} else if case .wholeMonth(let range) = selectionState, range.contains(dayDate) { | ||
wholeMonthRangeCell(range: range) | ||
} else { | ||
defaultCell | ||
} | ||
} | ||
|
||
@ViewBuilder private func singleView(selectionState: CalendarSingleSelectionState?) -> some View { | ||
switch selectionState { | ||
case .single(let date): | ||
if date == dayDate { | ||
singleCell(date: date) | ||
} else { | ||
defaultCell | ||
} | ||
case .wholeMonth(let closedRange, _): | ||
if closedRange.contains(dayDate) { | ||
rangeCell(closedRange: closedRange, highlightRangeEnds: false) | ||
} else { | ||
defaultCell | ||
} | ||
case .none: | ||
defaultCell | ||
} | ||
} | ||
|
||
private var defaultCell: some View { | ||
DefaultCalendarDayCell(calendar: calendar, date: dayDate) | ||
.accessibilityLabel(Text( | ||
accessibilityProvider.rangeSelection.accessibilityLabel(for: dayDate) | ||
)) | ||
} | ||
|
||
private func singleCell(date: Date) -> some View { | ||
SingleSelectedCell(calendar: calendar, date: dayDate) | ||
.accessibilityLabel( | ||
Text( | ||
accessibilityProvider.rangeSelection.accessibilityLabel( | ||
for: dayDate, | ||
intermediateSelectionDate: date | ||
) | ||
) | ||
) | ||
} | ||
|
||
private func rangeCell(closedRange: ClosedRange<Date>, highlightRangeEnds: Bool) -> some View { | ||
RangeSelectionCalendarDayCell( | ||
date: dayDate, | ||
selection: closedRange, | ||
calendar: calendar, | ||
highlightRangeEnds: highlightRangeEnds | ||
) | ||
.accessibilityLabel(Text( | ||
accessibilityProvider.rangeSelection.accessibilityLabel( | ||
for: dayDate, | ||
selection: closedRange | ||
) | ||
)) | ||
.accessibility(addTraits: .isSelected) | ||
} | ||
|
||
private func wholeMonthRangeCell(range: ClosedRange<Date>) -> some View { | ||
RangeSelectionCalendarDayCell( | ||
date: dayDate, | ||
selection: range, | ||
calendar: calendar, | ||
highlightRangeEnds: false | ||
) | ||
.accessibilityLabel(Text( | ||
accessibilityProvider.rangeSelection.accessibilityLabel( | ||
for: dayDate, | ||
selection: range | ||
) | ||
)) | ||
.accessibility(addTraits: .isSelected) | ||
} | ||
|
||
private func initialSelection(_ initialDateSelection: Date, matchesDate date: Date) -> Bool { | ||
let matchingDayComponents = calendar.dateComponents([.year, .month, .day], from: date) | ||
return calendar.date(initialDateSelection, matchesComponents: matchingDayComponents) | ||
cell.onTapGesture(perform: onSelection) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,8 +30,8 @@ struct CalendarMonthGrid< | |
|
||
@State private var dayCellHeight: CGFloat = 0 | ||
@ViewBuilder let dayCell: (Date) -> DayCell | ||
@ViewBuilder let emptyLeadingDayCell: (EmptyCellInfo) -> EmptyLeadingDayCell | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty cell info handled internally, as it's not something of interes outside this particualr view. |
||
@ViewBuilder let emptyTrailingDayCell: (EmptyCellInfo) -> EmptyTrailingDayCell | ||
@ViewBuilder let emptyLeadingDayCell: () -> EmptyLeadingDayCell | ||
@ViewBuilder let emptyTrailingDayCell: () -> EmptyTrailingDayCell | ||
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView | ||
|
||
private let daysInAWeek = 7 | ||
|
@@ -40,56 +40,74 @@ struct CalendarMonthGrid< | |
let firstWeekday = calendar.firstWeekday // Locale-aware first day of the week | ||
let weekdayOfMonthStart = calendar.component(.weekday, from: monthDate) | ||
// Calculate the offset based on the first weekday | ||
let weekdaysOffset = (weekdayOfMonthStart - firstWeekday + daysInAWeek) % daysInAWeek | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reverting these lines to where they were before |
||
let daysFromPreviousMonth = weekdaysOffset | ||
let emptyDaysLeading = (0..<daysFromPreviousMonth).map { index in | ||
EmptyCellInfo(cellIndex: index, month: monthDate) | ||
} | ||
|
||
let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: monthDate)!.count | ||
let totalCellsUsed = numberOfDaysInMonth + daysFromPreviousMonth | ||
let remainingCells = daysInAWeek - (totalCellsUsed % daysInAWeek) | ||
let emptyDaysTrailing = (0..<remainingCells).map { index in | ||
EmptyCellInfo(cellIndex: index, month: monthDate) | ||
} | ||
let daysFromPreviousMonth = (weekdayOfMonthStart - firstWeekday + daysInAWeek) % daysInAWeek | ||
|
||
LazyVGrid( | ||
columns: Array(repeating: GridItem(spacing: BPKSpacing.none.value), count: daysInAWeek), | ||
spacing: BPKSpacing.lg.value | ||
) { | ||
// Create cells for the days from the previous month that are shown in the first week of the current month. | ||
ForEach(emptyDaysLeading) { emptyDayInfo in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyLeadingDayCell(emptyDayInfo) | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
} | ||
|
||
previousEmptyCells(daysFromPreviousMonth: daysFromPreviousMonth) | ||
let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: monthDate)!.count | ||
// Create cells for the days in the current month | ||
currentMonthDayCell(numberOfDaysInMonth: numberOfDaysInMonth) | ||
|
||
// Create cells for the days from the next month that are shown in the last week of the current month | ||
// The total number of cells used is the sum of the number of days in the current month and the number of | ||
// days from the previous month that are shown | ||
|
||
if remainingCells < daysInAWeek { | ||
ForEach(emptyDaysTrailing) { emptyDayInfo in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyTrailingDayCell(emptyDayInfo) | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
let totalCellsUsed = numberOfDaysInMonth + daysFromPreviousMonth | ||
let remainingCells = daysInAWeek - (totalCellsUsed % daysInAWeek) | ||
|
||
remainingEmptyCells(remainingCells: remainingCells) | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private func previousEmptyCells(daysFromPreviousMonth: Int) -> some View { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted this from the above method to make it easier to read, also introducing the usage of a small internal struct DayCellIdentifiable, as the preiovus range only approach weas leading to mis-reusage of some cells, as they had repeating ids (which was a bug). |
||
let preEmptyCells = Array(0..<daysFromPreviousMonth) | ||
.map { | ||
DayCellIdentifiable(id: "pre-\($0)\(monthDate)", index: $0) | ||
} | ||
ForEach(preEmptyCells) { _ in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyLeadingDayCell() | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private func remainingEmptyCells(remainingCells: Int) -> some View { | ||
if remainingCells < daysInAWeek { | ||
let remainingEmptyCells = Array(0..<remainingCells) | ||
.map { | ||
DayCellIdentifiable(id: "rem-\($0)\(monthDate)", index: $0) | ||
} | ||
ForEach(remainingEmptyCells) { _ in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyTrailingDayCell() | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private struct DayCellIdentifiable: Identifiable { | ||
let id: String | ||
let index: Int | ||
} | ||
|
||
@ViewBuilder | ||
private func currentMonthDayCell(numberOfDaysInMonth: Int) -> some View { | ||
ForEach(0..<numberOfDaysInMonth, id: \.self) { cellIndex in | ||
let days = Array(0..<numberOfDaysInMonth) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above, using the struct rather than a ForEach with a range, that led to bugs before |
||
.map { | ||
DayCellIdentifiable(id: "\(monthDate)\($0)", index: $0) | ||
} | ||
ForEach(days) { cellIndex in | ||
let dayDate = calendar.date( | ||
byAdding: .init(day: cellIndex), | ||
byAdding: .init(day: cellIndex.index), | ||
to: monthDate | ||
)! | ||
|
||
|
@@ -123,8 +141,8 @@ struct CalendarMonthGrid_Previews: PreviewProvider { | |
dayCell: { day in | ||
BPKText("\(calendar.component(.day, from: day))") | ||
}, | ||
emptyLeadingDayCell: { _ in Color.red }, | ||
emptyTrailingDayCell: { _ in Color.green }, | ||
emptyLeadingDayCell: { Color.red }, | ||
emptyTrailingDayCell: { Color.green }, | ||
dayAccessoryView: { _ in | ||
BPKText("$200", style: .caption) | ||
.foregroundColor(.infoBannerSuccessColor) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now handled as it was before, as part of the binding itself, to not allow the API to lead to invalid states