[B2C-8878] Add month overlay item provider#3
Conversation
Add monthOverlayItemProvider API on CalendarViewContent that renders calendar item models on top of day content per month, complementing the existing monthBackgroundItemProvider which renders behind it. Introduce .monthOverlay item type through the visible-items pipeline, track its subview insertion index, and add a monthDaysAreaBounds computed property to MonthLayoutContext for convenient layout. Tests updated for new item type ordering. Made-with: Cursor
Add MonthOverlayDemoViewController showcasing the new monthOverlayItemProvider API with a blur-tint overlay on February months, and register it in the demo picker. Made-with: Cursor
…iewRepresentable Add monthOverlayItemProvider property and wire it into content creation. Provide both a CalendarItemModel-based modifier and a SwiftUI ViewBuilder-based monthOverlays(_:) convenience modifier. Made-with: Cursor
…Context The daysAndFrames comparison was comparing $0.frame to itself instead of $1.frame, causing two instances with different frames to be considered equal. Made-with: Cursor
…ay item handling Merge handleMonthBackgroundItemsIfNeeded and handleMonthOverlayItemsIfNeeded into a single iteration that builds the MonthLayoutContext once per month. Extract insertMonthItemIfNeeded helper to share the caching and insertion logic between background and overlay providers. Made-with: Cursor
…y demo Add deinit to MonthDaysTintOverlayView to explicitly stop the UIViewPropertyAnimator, avoiding a crash when the view is deallocated while the animator is still active. Made-with: Cursor
…erty ordering Fix indentation in monthOverlayItemProvider closure and deinit block, reorder MonthLayoutContext to place stored properties before computed ones, and minor style tweaks from PR review. Made-with: Cursor
…ction time Convert monthDaysAreaBounds from a computed property to a stored let, avoiding repeated O(n) iteration when consumers access it multiple times per layout pass (e.g. in both background and overlay providers). Made-with: Cursor
…afe optional Made-with: Cursor
| /// The bounding union rect of all day frames, or `nil` if there are no days in the month. | ||
| public let monthDaysAreaBounds: CGRect? | ||
|
|
||
| public static func ==(lhs: MonthLayoutContext, rhs: MonthLayoutContext) -> Bool { |
There was a problem hiding this comment.
question: monthDaysAreaBounds is not being taken into account in the Equality or the Hashability implementations (==, hash and so on). That's the downside of having custom equality or hashability implementations. You need to remember to update them with the corresponding property when adding a new property.
Since monthDaysAreaBounds is derived from daysAndFrames, which is present in ==, this may be safe, but worth checking: should we add monthDaysAreaBounds to the equality and hashable conformances? Even if we don't necessarily have to do it right now, the computation of monthDaysAreaBounds from daysAndFrames may drift in the future and cause problems.
I'm not sure, I would need more context on this from the low level library details, but worth checking.
There was a problem hiding this comment.
I got the same suggestion. I ended up not using monthDaysAreaBounds since it's derived from daysAndFrames. However, it does no harm adding it and it may prevent issues in the future if, as you say, computations drift in the future.
| self.invariantViewProperties = invariantViewProperties | ||
| super.init(frame: .zero) | ||
| backgroundColor = .clear | ||
| isUserInteractionEnabled = false |
There was a problem hiding this comment.
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 = false could 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.
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
| // We need to expand the frame of the month so that we have enough room at the edges to draw | ||
| // the background without getting clipped. |
There was a problem hiding this comment.
thought: I always prefer encapsulating methods in smaller functions with clear names over describing code with comments, but given that this is not our library and the code structure was like this from before, maybe the comments were useful? I don't know, just thinking out loud.
There was a problem hiding this comment.
Yeah let's keep them. We don't wanna come in someone's house and "redecorate" 😝
| let sortedDaysAndFrames = daysAndFrames.sorted(by: { $0.day < $1.day }) | ||
| let monthDaysAreaBounds = sortedDaysAndFrames.dropFirst().reduce( | ||
| sortedDaysAndFrames.first?.frame | ||
| ) { result, pair in | ||
| result?.union(pair.frame) | ||
| } |
There was a problem hiding this comment.
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:
- An overlay item is actually produced when the provider is set
- The overlay is not produced when the provider returns nil
- The monthDaysAreaBounds computation is correct
There was a problem hiding this comment.
I'll try to add those cases. Thanks!
| ) && | ||
| lhs.daysAndFrames.elementsEqual( | ||
| rhs.daysAndFrames, | ||
| by: { $0.day == $1.day && $0.frame == $0.frame } |
There was a problem hiding this comment.
praise: Well, it looks like you caught an equality mis-implementation bug from the original library :O 👏
There was a problem hiding this comment.
I'll take the praise because I prompted it hehe
…d Hashable conformance The custom `==` and `hash(into:)` implementations on `MonthLayoutContext` were missing the `monthDaysAreaBounds` property, which could cause two contexts with different day-area bounds to be treated as equal. Made-with: Cursor
… VisibleItemsProvider Made-with: Cursor
…eItemsProvider Cover three behaviors requested in PR review: - overlay items are produced when monthOverlayItemProvider returns a model - overlay items are excluded when monthOverlayItemProvider returns nil - monthDaysAreaBounds equals the union of all day frames for normal months and is nil for .noDays months Made-with: Cursor
…hDaysAreaBounds test Made-with: Cursor
ℹ️ Context
Ticket: B2C-8878
HorizonCalendar provides a
monthBackgroundItemProviderthat renders custom views behind day content for each month. However, there was no way to render custom views on top of day content — for example, semi-transparent tints, blur effects, or decorative overlays that should visually sit above the days.👷 Changes
monthOverlayItemProviderAPI onCalendarViewContent— works identically tomonthBackgroundItemProviderbut renders items on top of day views instead of behind them.monthOverlayItemProviderandmonthOverlaysmodifiers onCalendarViewRepresentable— SwiftUI-compatible API including a@ViewBuilder-basedmonthOverlaysmodifier for ergonomic SwiftUI usage.monthDaysAreaBoundsproperty onMonthLayoutContext— pre-computed bounding union rect of all day frames in a month, enabling overlay views to precisely target the days area..monthOverlaycase inVisibleItem.ItemTypewith proper z-ordering above day content but below pinned days-of-week and overlay items.handleMonthBackgroundAndOverlayItemsIfNeeded— deduplicated background and overlay item insertion into a sharedinsertMonthItemIfNeededhelper to avoid code duplication.MonthLayoutContextequality — corrected$0.frame == $0.frame(always true) to$0.frame == $1.framein thedaysAndFramesequality check.MonthOverlayDemoViewControllershowcasing a partial-intensity blur overlay on February months.isUserInteractionEnabled = false(UIKit) or.allowsHitTesting(false)(SwiftUI) is applied. This is documented but could surprise consumers.monthDaysAreaBoundsis computed eagerly for every visible month when either a background or overlay provider is set, even if neither provider uses it.🧪 How to Test
HorizonCalendarExample).SubviewInsertionIndexTrackerTestsandVisibleItemsProviderTestsshould pass.Made with Cursor
Note
Medium Risk
Medium risk because it changes core visible-item generation and subview z-ordering, which can affect rendering and touch handling for existing calendar content. API surface expands with new provider hooks that must remain stable and performant.
Overview
Adds a new
monthOverlayItemProvidercustomization point (and SwiftUImonthOverlaysmodifier) to render month-spanning decoration above day content, alongside the existing month background support.Updates internals to support the new
.monthOverlayvisible item type, including correct z-order insertion, shared month background/overlay item insertion logic, and a newMonthLayoutContext.monthDaysAreaBoundshelper for targeting only the days region (plus a smallMonthLayoutContextequality bug fix).Extends the example app with a new “Month Days Overlay” demo (blur/tint over February) and adds/updates unit tests to validate overlay presence/absence and ordering.
Written by Cursor Bugbot for commit 724189a. This will update automatically on new commits. Configure here.