Skip to content

Commit

Permalink
feat: Add accessory view to each day in the calendar (#2043)
Browse files Browse the repository at this point in the history
  • Loading branch information
frugoman authored Aug 8, 2024
1 parent 01813a1 commit 9a801db
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 61 deletions.
36 changes: 22 additions & 14 deletions Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,23 @@ import SwiftUI
///
/// The `BPKCalendar` view also allows you to specify an accessory action. This is a closure that takes a string and
/// a date, and is called when the user interacts with an accessory in the calendar.
public struct BPKCalendar: View {
public struct BPKCalendar<DayAccessoryView: View>: View {
let calendar: Calendar
let selectionType: CalendarSelectionType
let validRange: ClosedRange<Date>
private var accessoryAction: CalendarMonthAccessoryAction?
private let monthHeaderDateFormatter: DateFormatter

private let dayAccessoryView: (Date) -> DayAccessoryView
@State private var currentlyShownMonth: Date

public init(
selectionType: CalendarSelectionType,
calendar: Calendar,
validRange: ClosedRange<Date>
validRange: ClosedRange<Date>,
dayAccessoryView: @escaping (Date) -> DayAccessoryView = { _ in EmptyView() }
) {
self.dayAccessoryView = dayAccessoryView
_currentlyShownMonth = State(initialValue: validRange.lowerBound)
self.validRange = validRange
self.calendar = calendar
Expand All @@ -68,17 +71,19 @@ public struct BPKCalendar: View {
CalendarTypeContainerFactory(
selectionType: selectionType,
calendar: calendar,
validRange: validRange
) { monthDate in
CalendarMonthHeader(
monthDate: monthDate,
dateFormatter: monthHeaderDateFormatter,
calendar: calendar,
accessoryAction: accessoryAction,
currentlyShownMonth: $currentlyShownMonth,
parentProxy: calendarProxy
)
}
validRange: validRange,
monthHeader: { monthDate in
CalendarMonthHeader(
monthDate: monthDate,
dateFormatter: monthHeaderDateFormatter,
calendar: calendar,
accessoryAction: accessoryAction,
currentlyShownMonth: $currentlyShownMonth,
parentProxy: calendarProxy
)
},
dayAccessoryView: dayAccessoryView
)
yearBadge
}
}
Expand Down Expand Up @@ -127,7 +132,10 @@ struct BPKCalendar_Previews: PreviewProvider {
)
),
calendar: calendar,
validRange: minValidDate...maxValidDate
validRange: minValidDate...maxValidDate,
dayAccessoryView: { _ in
BPKText("20", style: .caption)
}
)
.monthAccessoryAction(CalendarMonthAccessoryAction(title: "Select whole month", action: { _ in }))
}
Expand Down
43 changes: 33 additions & 10 deletions Backpack-SwiftUI/Calendar/Classes/Core/CalendarMonthGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import SwiftUI
struct CalendarMonthGrid<
DayCell: View,
EmptyLeadingDayCell: View,
EmptyTrailingDayCell: View
EmptyTrailingDayCell: View,
DayAccessoryView: View
>: View {
let monthDate: Date
let calendar: Calendar
let validRange: ClosedRange<Date>

@State private var dayCellHeight: CGFloat = 0
@ViewBuilder let dayCell: (Date) -> DayCell
@ViewBuilder let emptyLeadingDayCell: () -> EmptyLeadingDayCell
@ViewBuilder let emptyTrailingDayCell: () -> EmptyTrailingDayCell

@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

private let daysInAWeek = 7

var body: some View {
Expand All @@ -47,7 +50,11 @@ struct CalendarMonthGrid<
) {
// Create cells for the days from the previous month that are shown in the first week of the current month.
ForEach(0..<daysFromPreviousMonth) { _ in
emptyLeadingDayCell()
VStack(spacing: BPKSpacing.none) {
emptyLeadingDayCell()
.frame(height: dayCellHeight)
Spacer(minLength: BPKSpacing.none)
}
}

let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: monthDate)!.count
Expand All @@ -62,7 +69,11 @@ struct CalendarMonthGrid<

if remainingCells < daysInAWeek {
ForEach(0..<remainingCells) { _ in
emptyTrailingDayCell()
VStack(spacing: BPKSpacing.none) {
emptyTrailingDayCell()
.frame(height: dayCellHeight)
Spacer(minLength: BPKSpacing.none)
}
}
}
}
Expand All @@ -77,9 +88,17 @@ struct CalendarMonthGrid<
)!

if !validRange.contains(dayDate) {
DisabledCalendarDayCell(calendar: calendar, date: dayDate)
VStack(spacing: BPKSpacing.none) {
DisabledCalendarDayCell(calendar: calendar, date: dayDate)
.frame(height: dayCellHeight)
Spacer(minLength: BPKSpacing.none)
}
} else {
dayCell(dayDate)
VStack(spacing: BPKSpacing.sm) {
dayCell(dayDate)
.modifier(ReadSizeModifier { dayCellHeight = $0.height })
dayAccessoryView(dayDate)
}
}
}
}
Expand All @@ -88,18 +107,22 @@ struct CalendarMonthGrid<
struct CalendarMonthGrid_Previews: PreviewProvider {
static var previews: some View {
let calendar = Calendar.current
let start = calendar.date(from: .init(year: 2023, month: 8, day: 30))!
let end = calendar.date(from: .init(year: 2028, month: 12, day: 25))!
let start = calendar.date(from: .init(year: 2023, month: 8, day: 25))!
let end = calendar.date(from: .init(year: 2023, month: 8, day: 28))!

CalendarMonthGrid(
monthDate: calendar.date(from: .init(year: 2023, month: 11, day: 1))!,
monthDate: calendar.date(from: .init(year: 2023, month: 8, day: 1))!,
calendar: calendar,
validRange: start...end,
dayCell: { day in
BPKText("\(calendar.component(.day, from: day))")
},
emptyLeadingDayCell: { Color.red },
emptyTrailingDayCell: { Color.green }
emptyTrailingDayCell: { Color.green },
dayAccessoryView: { _ in
BPKText("$200", style: .caption)
.foregroundColor(.infoBannerSuccessColor)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

import SwiftUI

struct CalendarTypeContainerFactory<MonthHeader: View>: View {
struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>: View {
let selectionType: CalendarSelectionType
let calendar: Calendar
let validRange: ClosedRange<Date>
@ViewBuilder let monthHeader: (_ monthDate: Date) -> MonthHeader
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

private var accessibilityDateFormatter: DateFormatter {
let formatter = DateFormatter()
Expand All @@ -42,7 +43,8 @@ struct CalendarTypeContainerFactory<MonthHeader: View>: View {
accessibilityConfigurations: accessibilityConfigurations,
dateFormatter: accessibilityDateFormatter
),
monthHeader: monthHeader
monthHeader: monthHeader,
dayAccessoryView: dayAccessoryView
)
case .single(let selected, let accessibilityConfigurations):
SingleCalendarContainer(
Expand All @@ -53,7 +55,8 @@ struct CalendarTypeContainerFactory<MonthHeader: View>: View {
accessibilityConfigurations: accessibilityConfigurations,
dateFormatter: accessibilityDateFormatter
),
monthHeader: monthHeader
monthHeader: monthHeader,
dayAccessoryView: dayAccessoryView
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@

import SwiftUI

struct RangeCalendarContainer<MonthHeader: View>: View {
struct RangeCalendarContainer<MonthHeader: View, DayAccessoryView: View>: View {
@Binding var selectionState: CalendarRangeSelectionState?
let calendar: Calendar
let validRange: ClosedRange<Date>
let accessibilityProvider: RangeDayAccessibilityProvider
@ViewBuilder let monthHeader: (_ monthDate: Date) -> MonthHeader
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

private func handleSelection(_ date: Date) {
switch selectionState {
Expand Down Expand Up @@ -82,22 +83,18 @@ struct RangeCalendarContainer<MonthHeader: View>: View {

@ViewBuilder
private func makeDayCell(_ dayDate: Date) -> some View {
if !validRange.contains(dayDate) {
DisabledCalendarDayCell(calendar: calendar, date: dayDate)
} else {
CalendarSelectableCell {
cell(dayDate)
} onSelection: {
handleSelection(dayDate)
}
.accessibilityHint(Text(
accessibilityProvider.accessibilityHint(
for: dayDate,
rangeSelectionState: selectionState
)
))
.accessibility(addTraits: .isButton)
CalendarSelectableCell {
cell(dayDate)
} onSelection: {
handleSelection(dayDate)
}
.accessibilityHint(Text(
accessibilityProvider.accessibilityHint(
for: dayDate,
rangeSelectionState: selectionState
)
))
.accessibility(addTraits: .isButton)
}

private func initialSelection(_ initialDateSelection: Date, matchesDate date: Date) -> Bool {
Expand All @@ -117,7 +114,8 @@ struct RangeCalendarContainer<MonthHeader: View>: View {
validRange: validRange,
dayCell: makeDayCell,
emptyLeadingDayCell: { makeEmptyLeadingDayCell(for: month) },
emptyTrailingDayCell: { makeEmptyTrailingDayCell(for: month) }
emptyTrailingDayCell: { makeEmptyTrailingDayCell(for: month) },
dayAccessoryView: dayAccessoryView
)
}
}
Expand Down Expand Up @@ -194,6 +192,9 @@ struct RangeCalendarContainer_Previews: PreviewProvider {
),
monthHeader: { month in
BPKText("\(Self.formatter.string(from: month))")
},
dayAccessoryView: { _ in
BPKText("20", style: .caption)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@

import SwiftUI

struct SingleCalendarContainer<MonthHeader: View>: View {
struct SingleCalendarContainer<MonthHeader: View, DayAccessoryView: View>: View {
@Binding var selection: Date?
let calendar: Calendar
let validRange: ClosedRange<Date>
let accessibilityProvider: SingleDayAccessibilityProvider
@ViewBuilder let monthHeader: (_ monthDate: Date) -> MonthHeader
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

@ViewBuilder
private func makeDayCell(_ dayDate: Date) -> some View {
Expand Down Expand Up @@ -51,7 +52,8 @@ struct SingleCalendarContainer<MonthHeader: View>: View {
validRange: validRange,
dayCell: makeDayCell,
emptyLeadingDayCell: { DefaultEmptyCalendarDayCell() },
emptyTrailingDayCell: { DefaultEmptyCalendarDayCell() }
emptyTrailingDayCell: { DefaultEmptyCalendarDayCell() },
dayAccessoryView: dayAccessoryView
)
}
}
Expand Down Expand Up @@ -79,6 +81,9 @@ struct SingleCalendarContainer_Previews: PreviewProvider {
),
monthHeader: { month in
BPKText("\(Self.formatter.string(from: month))")
},
dayAccessoryView: { _ in
BPKText("20", style: .caption)
}
)
}
Expand Down
16 changes: 16 additions & 0 deletions Backpack-SwiftUI/Calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,19 @@ BPKCalendar(
validRange: startDate...endDate
)
```

## Adding an accessory view to each day

An accessory view can be added to each day in the calendar. This can be used to display additional information or actions for each day.

```swift
BPKCalendar(
selectionType: .single(selected: $selectedDate),
calendar: .current,
validRange: startDate...endDate
dayAccessoryView: { date in
BPKText("$200 \(date)", style: .caption)
.foregroundColor(.accentColor)
}
)
```
21 changes: 21 additions & 0 deletions Backpack-SwiftUI/Tests/Calendar/BPKCalendarTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ class BPKCalendarTests: XCTestCase {
)
}

func test_rangeSelectionCalendar_withAccessoryView() {
let selectionStart = Calendar.current.date(from: DateComponents(year: 2020, month: 1, day: 28))!
let selectionEnd = Calendar.current.date(from: DateComponents(year: 2020, month: 2, day: 5))!

assertSnapshot(
BPKCalendar(
selectionType: .range(
selection: .constant(.range(selectionStart...selectionEnd)),
accessibilityConfigurations: rangeAccessibilityConfig
),
calendar: Calendar.current,
validRange: validStart...validEnd,
dayAccessoryView: { _ in
BPKIconView(.search, size: .small)
.foregroundColor(.accentColor)
}
)
.frame(width: 320, height: 720)
)
}

func test_rangeSelectionCalendar_sameDay() {
let selectionStart = Calendar.current.date(from: DateComponents(year: 2020, month: 2, day: 5))!
let selectionEnd = Calendar.current.date(from: DateComponents(year: 2020, month: 2, day: 5))!
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 9a801db

Please sign in to comment.