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

Conversation

frugoman
Copy link
Contributor

@frugoman frugoman commented Jan 3, 2025

Refactoring implementation to revert back to previous approach for composition of views for the BPKCalendar on SwiftUI
this refactor includes changes to support proper dynamic changes over the selection mode of the Calendar, which avoids re-renders of the entire calendar when selection mode is changed dynamically.

Also, the change includes a minor refactor that extracts the code for handling range selections, preparing for a tufure change that will require different handling of this depending on a flag's value

Note: Purposefully marking this as Minor, as this is reverting to the previous API, which was changed as a Minor change mistakenly. This aims to not break clients that bump minor versions expecting no api changes

New.changes.for.dynamic.calendar.mov

@frugoman frugoman enabled auto-merge January 3, 2025 19:22
@frugoman frugoman added the minor Non breaking change label Jan 3, 2025
@@ -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

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

@@ -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.

@@ -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
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

}

@ViewBuilder
private func previousEmptyCells(daysFromPreviousMonth: Int) -> some 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.

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).
Now we can set specific IDs to each cell and make sure they do not repeat across weeks/months in the calendar, fixing the bug where sometimes the cells were not rendering correctly

@ViewBuilder
private func currentMonthDayCell(numberOfDaysInMonth: Int) -> some View {
ForEach(0..<numberOfDaysInMonth, id: \.self) { cellIndex in
let days = Array(0..<numberOfDaysInMonth)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

@@ -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

Comment on lines -63 to -79
}

func returnMakeCellFunction() -> ((Date) -> CalendarSelectableCell) {
return { dayDate in
CalendarSelectableCell(
selectionType: selectionType,
calendar: calendar,
accessibilityProvider: calendarAccessibilityConfiguration,
dayDate: dayDate,
onSelection: handleSelection
)
}

}

@ViewBuilder func emptyLeadingDayCell(for emptyDayInfo: EmptyCellInfo) -> some View {
switch selectionType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most this changes are reverting back to the old implementation. with an exception mentioned below

} else {
// otherwise we occupy the space with a clear view
DefaultEmptyCalendarDayCell()
switch selectionType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the relevant change here (the point of this PR) is that the CalendarContainer is now here, wrapping both the single and range selection calendar month containers, instead of the CalendarContainer being inside the single and range respectively. This is what makes the change of selection types dynamically not re-render the entire calendar, as the only thing that's now re-rendered is the months themselves, and not the entire scrollview.

For context, the problem was that before when you changed selection types of the calendar, it'd re-render the whole thing as this switch was the entire calendar including the scrollviews, now the only thing that is re rendered is the months's grids, WITHIN the container scrollview, making the changes to this vlaue NOT trigger a scroll 'reset' 👍

Copy link
Contributor Author

@frugoman frugoman Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So essentially:
now:

Container { <--- ScrollView
  Range { 
    Grid
  }
  or...
  Single {
    Grid
  }
}

before:

Range {
  Container { <--- ScrollView
    Grid
  }
}
or...
Single {
  Container { <--- ScrollView
    Grid
  }
}

Copy link
Contributor

github-actions bot commented Jan 3, 2025

Snapshots were updated. Please verify the changes match the expected layout.

' D Backpack-SwiftUI/Tests/BannerAlert/Snapshots/BPKBannerAlertTests/test_multilineBannerAlert.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/BannerAlert/Snapshots/BPKBannerAlertTests/test_multilineBannerAlert.light-mode.png'%0A' D Backpack-SwiftUI/Tests/BannerAlert/Snapshots/BPKBannerAlertTests/test_multilineBannerAlert.rtl.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_dark.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_dark.light-mode.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_dark.rtl.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_default.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_default.light-mode.png'%0A' D Backpack-SwiftUI/Tests/NavigationTabGroup/Snapshots/BPKNavigationTabGroupTests/test_default.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_clearableField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_clearableField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_clearableField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_disabledField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_errorField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/SearchInputSummary/Snapshots/BPKSearchInputSummaryTests/test_validField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_clearableField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_defaultSettings_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_disabledField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_emptyField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_errorField_withPrefixIcon.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withCustomPrefixText.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withCustomPrefixText.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withCustomPrefixText.rtl.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withPrefixIcon.dark-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withPrefixIcon.light-mode.png'%0A' D Backpack-SwiftUI/Tests/TextField/Snapshots/BPKTextFieldTests/test_validField_withPrefixIcon.rtl.png'


import SwiftUI

protocol RangeCalendarSelectionHandler {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introducing this protocol so that we can extract the selection handling logic from the Container view and eventually swap it with a new implementation, which is a change that's coming for the range selection calendars, which will be experimented upon.

When the new seleciton logic is implemented, we should only add a new class that conforms to this protocol and, if flag is enabled use one or the other implementaion.

dateFormatter: accessibilityDateFormatter
),
month: month,
selectionHandler: DefaultRangeCalendarSelectionHandler(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When new one is implemented later on, here we'd have something like:

selectionHandler: flagEnabled ? NewHandler() : DefaultHandler()


import SwiftUI

struct SingleCalendarMonthContainer<MonthHeader: View, DayAccessoryView: 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.

nothng to see here, just reverting

import SwiftUI
import Backpack_SwiftUI

struct CalendarExampleDynamicView: 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.

created this example to play around the idea of dynamically changing the selection mode of the calendar, which is now better supported wityh this pr

@frugoman frugoman merged commit e7b7ed2 into main Jan 6, 2025
14 checks passed
@frugoman frugoman deleted the revert-2128-donburi/DON-1048_m1b_flightsdateselector branch January 6, 2025 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
minor Non breaking change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants