-
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 1 commit
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 | ||
|
@@ -42,41 +42,34 @@ struct CalendarMonthGrid< | |
// 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) | ||
} | ||
|
||
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 | ||
ForEach(0..<daysFromPreviousMonth) { _ in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyLeadingDayCell(emptyDayInfo) | ||
emptyLeadingDayCell() | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
} | ||
|
||
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 | ||
|
||
let totalCellsUsed = numberOfDaysInMonth + daysFromPreviousMonth | ||
let remainingCells = daysInAWeek - (totalCellsUsed % daysInAWeek) | ||
|
||
if remainingCells < daysInAWeek { | ||
ForEach(emptyDaysTrailing) { emptyDayInfo in | ||
ForEach(0..<remainingCells) { _ in | ||
VStack(spacing: BPKSpacing.none) { | ||
emptyTrailingDayCell(emptyDayInfo) | ||
emptyTrailingDayCell() | ||
.frame(height: dayCellHeight) | ||
Spacer(minLength: BPKSpacing.none) | ||
} | ||
|
@@ -123,8 +116,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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,23 +18,13 @@ | |
|
||
import SwiftUI | ||
|
||
public struct CalendarAccessibilityConfiguration { | ||
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. reverted this to the way it was handled before, reducing duplication and potential invalid states |
||
public let singleSelection: SingleDayAccessibilityProvider | ||
public let rangeSelection: RangeDayAccessibilityProvider | ||
public init(singleSelection: SingleDayAccessibilityProvider, rangeSelection: RangeDayAccessibilityProvider) { | ||
self.singleSelection = singleSelection | ||
self.rangeSelection = rangeSelection | ||
} | ||
} | ||
|
||
struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>: View { | ||
let selectionType: CalendarSelectionType | ||
let calendar: Calendar | ||
let validRange: ClosedRange<Date> | ||
let monthScroll: MonthScroll? | ||
@ViewBuilder let monthHeader: (_ monthDate: Date) -> MonthHeader | ||
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView | ||
var calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration | ||
|
||
private var accessibilityDateFormatter: DateFormatter { | ||
let formatter = DateFormatter() | ||
|
@@ -44,102 +34,34 @@ struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>: | |
} | ||
|
||
var body: some View { | ||
CalendarContainer( | ||
calendar: calendar, | ||
validRange: validRange, | ||
monthScroll: monthScroll | ||
) { month in | ||
monthHeader(month) | ||
CalendarMonthGrid( | ||
monthDate: month, | ||
switch selectionType { | ||
case .range(let selection, let accessibilityConfigurations): | ||
RangeCalendarContainer( | ||
selectionState: selection, | ||
calendar: calendar, | ||
validRange: validRange, | ||
dayCell: returnMakeCellFunction(), | ||
emptyLeadingDayCell: emptyLeadingDayCell, | ||
emptyTrailingDayCell: emptyTrailingDayCell, | ||
accessibilityProvider: RangeDayAccessibilityProvider( | ||
accessibilityConfigurations: accessibilityConfigurations, | ||
dateFormatter: accessibilityDateFormatter | ||
), | ||
monthScroll: monthScroll, | ||
monthHeader: monthHeader, | ||
dayAccessoryView: dayAccessoryView | ||
) | ||
} | ||
} | ||
|
||
func returnMakeCellFunction() -> ((Date) -> CalendarSelectableCell) { | ||
return { dayDate in | ||
CalendarSelectableCell( | ||
selectionType: selectionType, | ||
case .single(let selection, let accessibilityConfigurations): | ||
SingleCalendarContainer( | ||
selection: selection, | ||
calendar: calendar, | ||
accessibilityProvider: calendarAccessibilityConfiguration, | ||
dayDate: dayDate, | ||
onSelection: handleSelection | ||
validRange: validRange, | ||
accessibilityProvider: SingleDayAccessibilityProvider( | ||
accessibilityConfigurations: accessibilityConfigurations, | ||
dateFormatter: accessibilityDateFormatter | ||
), | ||
monthScroll: monthScroll, | ||
monthHeader: monthHeader, | ||
dayAccessoryView: dayAccessoryView | ||
) | ||
} | ||
|
||
} | ||
|
||
@ViewBuilder func emptyLeadingDayCell(for emptyDayInfo: EmptyCellInfo) -> some View { | ||
switch selectionType { | ||
case .range(let selection, _): | ||
if | ||
case .range(let selectionRange) = selection.wrappedValue, | ||
let lastDayOfPreviousMonth = calendar.date(byAdding: .init(day: -1), to: emptyDayInfo.month), | ||
let firstDayOfCurrentMonth = calendar.date(byAdding: .init(day: 1), to: lastDayOfPreviousMonth), | ||
selectionRange.contains(lastDayOfPreviousMonth), | ||
selectionRange.contains(firstDayOfCurrentMonth) | ||
{ | ||
Color(.surfaceSubtleColor) | ||
} else { | ||
// otherwise we occupy the space with a clear view | ||
DefaultEmptyCalendarDayCell() | ||
} | ||
case .single: | ||
DefaultEmptyCalendarDayCell() | ||
} | ||
} | ||
|
||
@ViewBuilder func emptyTrailingDayCell(for emptyDayInfo: EmptyCellInfo) -> some View { | ||
switch selectionType { | ||
case .range(let selection, _): | ||
if | ||
case .range(let selectionRange) = selection.wrappedValue, | ||
let firstDayOfNextMonth = calendar.date(byAdding: .init(month: 1), to: emptyDayInfo.month), | ||
let lastDayOfCurrentMonth = calendar.date(byAdding: .init(day: -1), to: firstDayOfNextMonth), | ||
selectionRange.contains(lastDayOfCurrentMonth), | ||
selectionRange.contains(firstDayOfNextMonth) | ||
{ | ||
Color(.surfaceSubtleColor) | ||
} else { | ||
// otherwise we occupy the space with a clear view | ||
DefaultEmptyCalendarDayCell() | ||
} | ||
case .single: | ||
DefaultEmptyCalendarDayCell() | ||
} | ||
} | ||
|
||
func handleSelection(dayDate: Date) { | ||
switch selectionType { | ||
case .range(let selection, _): | ||
switch selection.wrappedValue { | ||
case .intermediate(let initialDateSelection): | ||
if dayDate < initialDateSelection { | ||
selection.wrappedValue = .intermediate(dayDate) | ||
UIAccessibility.post( | ||
notification: .announcement, | ||
argument: calendarAccessibilityConfiguration.rangeSelection | ||
.accessibilityInstructionAfterSelectingDate() | ||
) | ||
} else { | ||
selection.wrappedValue = .range(initialDateSelection...dayDate) | ||
} | ||
default: | ||
selection.wrappedValue = .intermediate(dayDate) | ||
UIAccessibility.post( | ||
notification: .announcement, | ||
argument: calendarAccessibilityConfiguration.rangeSelection | ||
.accessibilityInstructionAfterSelectingDate() | ||
) | ||
} | ||
case .single(let selection, _): | ||
selection.wrappedValue = .single(dayDate) | ||
|
||
} | ||
} | ||
} |
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