-
Notifications
You must be signed in to change notification settings - Fork 0
[B2C-8878] Add month overlay item provider #3
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
Changes from all commits
70155a1
5f5ffd8
40e6aa5
2b87b27
8cb5cea
0ddfee2
a234168
038f38e
fe2b566
6007b14
412dd27
b739798
724189a
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 |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| // Created by HorizonCalendar contributors. | ||
| // Copyright © 2024 Airbnb Inc. All rights reserved. | ||
|
|
||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import HorizonCalendar | ||
| import UIKit | ||
|
|
||
| // MARK: - MonthOverlayDemoViewController | ||
|
|
||
| final class MonthOverlayDemoViewController: BaseDemoViewController { | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| title = "Month Days Overlay" | ||
| } | ||
|
|
||
| override func makeContent() -> CalendarViewContent { | ||
| let startDate = calendar.date(from: DateComponents(year: 2020, month: 01, day: 10))! | ||
| let endDate = calendar.date(from: DateComponents(year: 2021, month: 12, day: 31))! | ||
|
|
||
| return CalendarViewContent( | ||
| calendar: calendar, | ||
| visibleDateRange: startDate...endDate, | ||
| monthsLayout: monthsLayout | ||
| ) | ||
| .interMonthSpacing(24) | ||
| .verticalDayMargin(8) | ||
| .horizontalDayMargin(8) | ||
| .monthOverlayItemProvider { monthLayoutContext in | ||
| guard monthLayoutContext.month.month == 2 else { return nil } | ||
| return MonthDaysTintOverlayView.calendarItemModel( | ||
| invariantViewProperties: .init(), | ||
| content: .init(daysRect: monthLayoutContext.monthDaysAreaBounds) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| // MARK: - MonthDaysTintOverlayView | ||
|
|
||
| final class MonthDaysTintOverlayView: UIView { | ||
|
|
||
| // MARK: Lifecycle | ||
|
|
||
| fileprivate init(invariantViewProperties: InvariantViewProperties) { | ||
| self.invariantViewProperties = invariantViewProperties | ||
| super.init(frame: .zero) | ||
| backgroundColor = .clear | ||
| isUserInteractionEnabled = false | ||
|
|
||
| blurView.clipsToBounds = true | ||
| addSubview(blurView) | ||
|
|
||
| blurAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [blurView] in | ||
| blurView.effect = UIBlurEffect(style: invariantViewProperties.blurStyle) | ||
| } | ||
| blurAnimator?.fractionComplete = invariantViewProperties.blurIntensity | ||
| blurAnimator?.pausesOnCompletion = true | ||
| } | ||
|
|
||
| required init?(coder _: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| deinit { | ||
| blurAnimator?.stopAnimation(true) | ||
| } | ||
|
|
||
| // MARK: Internal | ||
|
|
||
| override func layoutSubviews() { | ||
| super.layoutSubviews() | ||
| blurView.frame = daysRect ?? .zero | ||
| blurView.isHidden = daysRect == nil | ||
| } | ||
|
|
||
| // MARK: Fileprivate | ||
|
|
||
| fileprivate var daysRect: CGRect? { | ||
| didSet { | ||
| guard daysRect != oldValue else { return } | ||
| setNeedsLayout() | ||
| } | ||
| } | ||
|
|
||
| // MARK: Private | ||
|
|
||
| private let invariantViewProperties: InvariantViewProperties | ||
| private let blurView = UIVisualEffectView() | ||
| private var blurAnimator: UIViewPropertyAnimator? | ||
|
|
||
| } | ||
|
|
||
| // MARK: CalendarItemViewRepresentable | ||
|
|
||
| extension MonthDaysTintOverlayView: CalendarItemViewRepresentable { | ||
|
|
||
| struct InvariantViewProperties: Hashable { | ||
| var blurStyle = UIBlurEffect.Style.extraLight | ||
| var blurIntensity: CGFloat = 0.1 | ||
| } | ||
|
|
||
| struct Content: Equatable { | ||
| let daysRect: CGRect? | ||
| } | ||
|
|
||
| static func makeView( | ||
| withInvariantViewProperties invariantViewProperties: InvariantViewProperties | ||
| ) -> MonthDaysTintOverlayView { | ||
| MonthDaysTintOverlayView(invariantViewProperties: invariantViewProperties) | ||
| } | ||
|
|
||
| static func setContent(_ content: Content, on view: MonthDaysTintOverlayView) { | ||
| view.daysRect = content.daysRect | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -295,8 +295,8 @@ final class VisibleItemsProvider { | |
| // Handle overlay items | ||
| handleOverlayItemsIfNeeded(bounds: bounds, context: &context) | ||
|
|
||
| // Handle background items | ||
| handleMonthBackgroundItemsIfNeeded(context: &context) | ||
| // Handle month background and overlay items | ||
| handleMonthBackgroundAndOverlayItemsIfNeeded(context: &context) | ||
|
|
||
| previousHeightsForVisibleMonthHeaders = context.heightsForVisibleMonthHeaders | ||
| previousCalendarItemModelCache = context.calendarItemModelCache | ||
|
|
@@ -715,7 +715,8 @@ final class VisibleItemsProvider { | |
| ) | ||
|
|
||
| let layoutNonVisibleItemsInPartiallyVisibleMonth = content.monthsLayout.isHorizontal || | ||
| content.monthBackgroundItemProvider != nil | ||
| content.monthBackgroundItemProvider != nil || | ||
| content.monthOverlayItemProvider != nil | ||
|
|
||
| if | ||
| layoutItem.frame.intersects(extendedBounds) || | ||
|
|
@@ -1056,8 +1057,12 @@ final class VisibleItemsProvider { | |
| } | ||
| } | ||
|
|
||
| private func handleMonthBackgroundItemsIfNeeded(context: inout VisibleItemsContext) { | ||
| guard let monthBackgroundItemProvider = content.monthBackgroundItemProvider else { return } | ||
| private func handleMonthBackgroundAndOverlayItemsIfNeeded( | ||
| context: inout VisibleItemsContext | ||
| ) { | ||
| let backgroundProvider = content.monthBackgroundItemProvider | ||
| let overlayProvider = content.monthOverlayItemProvider | ||
| guard backgroundProvider != nil || overlayProvider != nil else { return } | ||
|
|
||
| for (month, monthFrame) in context.framesForVisibleMonths { | ||
| let framesForDays: [Day: CGRect] | ||
|
|
@@ -1077,11 +1082,11 @@ final class VisibleItemsProvider { | |
| let extraWidth: CGFloat | ||
| let extraHeight: CGFloat | ||
| if content.monthsLayout.isHorizontal { | ||
| extraWidth = content.interMonthSpacing // half before leading edge, half after trailing edge | ||
| extraWidth = content.interMonthSpacing | ||
| extraHeight = size.height - monthFrame.height | ||
| } else { | ||
| extraWidth = size.width - monthFrame.width | ||
| extraHeight = content.interMonthSpacing // half before top edge, half after bottom edge | ||
| extraHeight = content.interMonthSpacing | ||
| } | ||
|
|
||
| let expandedMonthFrame = CGRect( | ||
|
|
@@ -1129,30 +1134,58 @@ final class VisibleItemsProvider { | |
| daysAndFrames.append((day, finalDayFrame)) | ||
| } | ||
|
|
||
| let sortedDaysAndFrames = daysAndFrames.sorted(by: { $0.day < $1.day }) | ||
| let monthDaysAreaBounds = sortedDaysAndFrames.dropFirst().reduce( | ||
| sortedDaysAndFrames.first?.frame | ||
| ) { result, pair in | ||
| result?.union(pair.frame) | ||
| } | ||
|
Comment on lines
+1137
to
+1142
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. question: Would it be possible to add test coverage for these new computations? Only if possible. I'm not sure if these are testable the way the library is, but some cases where I think we are not adding test coverage include:
Collaborator
Author
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. I'll try to add those cases. Thanks! |
||
|
|
||
| let monthLayoutContext = MonthLayoutContext( | ||
| month: month, | ||
| monthHeaderFrame: finalMonthHeaderFrame, | ||
| dayOfWeekPositionsAndFrames: dayOfWeekPositionsAndFrames, | ||
| daysAndFrames: daysAndFrames.sorted(by: { $0.day < $1.day }), | ||
| bounds: CGRect(origin: .zero, size: expandedMonthFrame.size) | ||
| daysAndFrames: sortedDaysAndFrames, | ||
| bounds: CGRect(origin: .zero, size: expandedMonthFrame.size), | ||
| monthDaysAreaBounds: monthDaysAreaBounds | ||
| ) | ||
|
|
||
| let itemType = VisibleItem.ItemType.monthBackground(month) | ||
| let itemModel = context.calendarItemModelCache.optionalValue( | ||
| for: itemType, | ||
| missingValueProvider: { | ||
| previousCalendarItemModelCache?[itemType] ?? | ||
| monthBackgroundItemProvider(monthLayoutContext) | ||
| } | ||
| insertMonthItemIfNeeded( | ||
| itemType: .monthBackground(month), | ||
| provider: backgroundProvider, | ||
| monthLayoutContext: monthLayoutContext, | ||
| frame: expandedMonthFrame, | ||
| context: &context | ||
| ) | ||
| if let itemModel { | ||
| let visibleItem = VisibleItem( | ||
| calendarItemModel: itemModel, | ||
| itemType: itemType, | ||
| frame: expandedMonthFrame | ||
| ) | ||
| context.visibleItems.insert(visibleItem) | ||
|
|
||
| insertMonthItemIfNeeded( | ||
| itemType: .monthOverlay(month), | ||
| provider: overlayProvider, | ||
| monthLayoutContext: monthLayoutContext, | ||
| frame: expandedMonthFrame, | ||
| context: &context | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func insertMonthItemIfNeeded( | ||
| itemType: VisibleItem.ItemType, | ||
| provider: ((MonthLayoutContext) -> AnyCalendarItemModel?)?, | ||
| monthLayoutContext: MonthLayoutContext, | ||
| frame: CGRect, | ||
| context: inout VisibleItemsContext | ||
| ) { | ||
| guard let provider else { return } | ||
| let itemModel = context.calendarItemModelCache.optionalValue( | ||
| for: itemType, | ||
| missingValueProvider: { | ||
| previousCalendarItemModelCache?[itemType] ?? provider(monthLayoutContext) | ||
| } | ||
| ) | ||
| if let itemModel { | ||
| context.visibleItems.insert( | ||
| VisibleItem(calendarItemModel: itemModel, itemType: itemType, frame: frame) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
|
|
||
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.
question: I noticed that this property was set in the example view controller and made me think: should this be the default behavior? If overlays are not meant to be interactable from an API design standpoint, it may be easier for client code to not have to set this explicitly, so if it's a sensible default then
isUserInteractionEnabled = falsecould be set by the library by default, not the client code. The goal with these is always to try to make client code integration as easy as possible, so that they don't have to "remember" to set UI configs like that, whenever possible.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.
Yeah I considered that but, contrary to a background, I think an overlay might actually need to be interactive. I'm thinking a "reload" button for example