Skip to content
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

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 1 addition & 23 deletions Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
private var accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)?
private var initialMonthScroll: MonthScroll?
private let monthHeaderDateFormatter: DateFormatter
private let calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration

private let dayAccessoryView: (Date) -> DayAccessoryView
@State private var currentlyShownMonth: Date
Expand All @@ -50,15 +49,13 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
calendar: Calendar,
validRange: ClosedRange<Date>,
initialMonthScroll: MonthScroll? = nil,
calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration,
Copy link
Contributor Author

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

dayAccessoryView: @escaping (Date) -> DayAccessoryView = { _ in EmptyView() }
) {
self.dayAccessoryView = dayAccessoryView
_currentlyShownMonth = State(initialValue: validRange.lowerBound)
self.validRange = validRange
self.calendar = calendar
self.selectionType = selectionType
self.calendarAccessibilityConfiguration = calendarAccessibilityConfiguration
self.initialMonthScroll = initialMonthScroll

monthHeaderDateFormatter = DateFormatter()
Expand Down Expand Up @@ -91,8 +88,7 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
parentProxy: calendarProxy
)
},
dayAccessoryView: dayAccessoryView,
calendarAccessibilityConfiguration: calendarAccessibilityConfiguration
dayAccessoryView: dayAccessoryView
)
yearBadge
}
Expand Down Expand Up @@ -143,24 +139,6 @@ struct BPKCalendar_Previews: PreviewProvider {
),
calendar: calendar,
validRange: minValidDate...maxValidDate,
calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration(
singleSelection: .init(
accessibilityConfigurations: .init(selectionHint: "hint"),
dateFormatter: DateFormatter()
),
rangeSelection: .init(
accessibilityConfigurations: .init(
startSelectionHint: "startSelectionHint",
endSelectionHint: "endSelectionHint",
startSelectionState: "startSelectionState",
endSelectionState: "endSelectionState",
betweenSelectionState: "betweenSelectionState",
startAndEndSelectionState: "startAndEndSelectionState",
returnDatePrompt: "returnDatePrompt"
),
dateFormatter: DateFormatter()
)
),
dayAccessoryView: { _ in
BPKText("20", style: .caption)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
}
}
31 changes: 12 additions & 19 deletions Backpack-SwiftUI/Calendar/Classes/Core/CalendarMonthGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ struct CalendarMonthGrid<

@State private var dayCellHeight: CGFloat = 0
@ViewBuilder let dayCell: (Date) -> DayCell
@ViewBuilder let emptyLeadingDayCell: (EmptyCellInfo) -> EmptyLeadingDayCell
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand All @@ -42,41 +42,34 @@ struct CalendarMonthGrid<
// Calculate the offset based on the first weekday
let weekdaysOffset = (weekdayOfMonthStart - firstWeekday + daysInAWeek) % daysInAWeek
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
}
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,13 @@

import SwiftUI

public struct CalendarAccessibilityConfiguration {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
Expand All @@ -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)

}
}
}
Loading
Loading