Skip to content

[B2C-8878] Add month overlay item provider#3

Merged
liebanajr merged 13 commits intomasterfrom
feature/B2C-8878_add_month_overlay
Mar 20, 2026
Merged

[B2C-8878] Add month overlay item provider#3
liebanajr merged 13 commits intomasterfrom
feature/B2C-8878_add_month_overlay

Conversation

@liebanajr
Copy link
Copy Markdown
Collaborator

@liebanajr liebanajr commented Mar 19, 2026

ℹ️ Context

Ticket: B2C-8878

HorizonCalendar provides a monthBackgroundItemProvider that 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

  • New monthOverlayItemProvider API on CalendarViewContent — works identically to monthBackgroundItemProvider but renders items on top of day views instead of behind them.
  • New monthOverlayItemProvider and monthOverlays modifiers on CalendarViewRepresentable — SwiftUI-compatible API including a @ViewBuilder-based monthOverlays modifier for ergonomic SwiftUI usage.
  • New monthDaysAreaBounds property on MonthLayoutContext — pre-computed bounding union rect of all day frames in a month, enabling overlay views to precisely target the days area.
  • New .monthOverlay case in VisibleItem.ItemType with proper z-ordering above day content but below pinned days-of-week and overlay items.
  • Refactored handleMonthBackgroundAndOverlayItemsIfNeeded — deduplicated background and overlay item insertion into a shared insertMonthItemIfNeeded helper to avoid code duplication.
  • Bug fix in MonthLayoutContext equality — corrected $0.frame == $0.frame (always true) to $0.frame == $1.frame in the daysAndFrames equality check.
  • Example demo — new MonthOverlayDemoViewController showcasing a partial-intensity blur overlay on February months.

⚠️ Trade-offs & Tech Debt

  • Overlay views are rendered above day views and will intercept touches unless isUserInteractionEnabled = false (UIKit) or .allowsHitTesting(false) (SwiftUI) is applied. This is documented but could surprise consumers.
  • The monthDaysAreaBounds is computed eagerly for every visible month when either a background or overlay provider is set, even if neither provider uses it.

🧪 How to Test

  1. Open the example app (HorizonCalendarExample).
  2. Navigate to the "Month Days Overlay" demo from the picker.
  3. Verify February months display a subtle blur overlay above the day cells.
  4. Verify non-February months have no overlay.
  5. Scroll through months — overlay should appear/disappear smoothly.
  6. Tap day cells on non-February months to confirm they remain tappable.
  7. Verify the existing "Month Grid Background" demo still works correctly (no regressions).
  8. Run the test suite — SubviewInsertionIndexTrackerTests and VisibleItemsProviderTests should 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 monthOverlayItemProvider customization point (and SwiftUI monthOverlays modifier) to render month-spanning decoration above day content, alongside the existing month background support.

Updates internals to support the new .monthOverlay visible item type, including correct z-order insertion, shared month background/overlay item insertion logic, and a new MonthLayoutContext.monthDaysAreaBounds helper for targeting only the days region (plus a small MonthLayoutContext equality 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.

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
@liebanajr liebanajr added the enhancement New feature or request label Mar 19, 2026
@liebanajr liebanajr self-assigned this Mar 19, 2026
@liebanajr liebanajr requested a review from drogelfever March 19, 2026 15:42
Comment on lines +45 to 48
/// 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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown

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

Copy link
Copy Markdown
Collaborator Author

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

Comment on lines -1075 to -1076
// 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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah let's keep them. We don't wanna come in someone's house and "redecorate" 😝

Comment on lines +1132 to +1137
let sortedDaysAndFrames = daysAndFrames.sorted(by: { $0.day < $1.day })
let monthDaysAreaBounds = sortedDaysAndFrames.dropFirst().reduce(
sortedDaysAndFrames.first?.frame
) { result, pair in
result?.union(pair.frame)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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:

  • 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'll try to add those cases. Thanks!

) &&
lhs.daysAndFrames.elementsEqual(
rhs.daysAndFrames,
by: { $0.day == $1.day && $0.frame == $0.frame }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

praise: Well, it looks like you caught an equality mis-implementation bug from the original library :O 👏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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
…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
@liebanajr liebanajr requested a review from drogelfever March 20, 2026 07:42
@liebanajr liebanajr merged commit 9a625e3 into master Mar 20, 2026
3 checks passed
@liebanajr liebanajr deleted the feature/B2C-8878_add_month_overlay branch March 20, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants