Skip to content

Commit

Permalink
Merge pull request #2130 from Skyscanner/revert-2128-donburi/DON-1048…
Browse files Browse the repository at this point in the history
…_m1b_flightsdateselector

Refactor calendar to support dynamic selection type changes
  • Loading branch information
frugoman authored Jan 6, 2025
2 parents e55955b + 26a38d0 commit e7b7ed2
Show file tree
Hide file tree
Showing 16 changed files with 582 additions and 434 deletions.
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,
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 {
@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)
}
}
86 changes: 52 additions & 34 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
@ViewBuilder let emptyTrailingDayCell: (EmptyCellInfo) -> EmptyTrailingDayCell
@ViewBuilder let emptyLeadingDayCell: () -> EmptyLeadingDayCell
@ViewBuilder let emptyTrailingDayCell: () -> EmptyTrailingDayCell
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

private let daysInAWeek = 7
Expand All @@ -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
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 {
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)
.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
)!

Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit e7b7ed2

Please sign in to comment.