Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
939E69942484D55200A8BCC7 /* HorizonCalendar.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 939E69912484D11000A8BCC7 /* HorizonCalendar.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
93A7258E24A1F26C00B4F08F /* PartialMonthVisibilityDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A7258D24A1F26C00B4F08F /* PartialMonthVisibilityDemoViewController.swift */; };
93AF5545248DCC8900BDB0FF /* DayRangeIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93AF5544248DCC8900BDB0FF /* DayRangeIndicatorView.swift */; };
9503CCD72F6BFAA400883FC3 /* MonthOverlayDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9503CCD62F6BFAA400883FC3 /* MonthOverlayDemoViewController.swift */; };
FD53899F299476AD007D56EB /* DayRangeSelectionTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD53899E299476AD007D56EB /* DayRangeSelectionTracker.swift */; };
FD55C5D7298B138A00A9B5D6 /* SwiftUIScreenDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD55C5D6298B138A00A9B5D6 /* SwiftUIScreenDemoViewController.swift */; };
FDA0FB3528F5EFD90066DEFA /* SwiftUIDayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA0FB3428F5EFD90066DEFA /* SwiftUIDayView.swift */; };
Expand Down Expand Up @@ -60,6 +61,7 @@
939E69912484D11000A8BCC7 /* HorizonCalendar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = HorizonCalendar.framework; sourceTree = BUILT_PRODUCTS_DIR; };
93A7258D24A1F26C00B4F08F /* PartialMonthVisibilityDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialMonthVisibilityDemoViewController.swift; sourceTree = "<group>"; };
93AF5544248DCC8900BDB0FF /* DayRangeIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayRangeIndicatorView.swift; sourceTree = "<group>"; };
9503CCD62F6BFAA400883FC3 /* MonthOverlayDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthOverlayDemoViewController.swift; sourceTree = "<group>"; };
FD53899E299476AD007D56EB /* DayRangeSelectionTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayRangeSelectionTracker.swift; sourceTree = "<group>"; };
FD55C5D6298B138A00A9B5D6 /* SwiftUIScreenDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIScreenDemoViewController.swift; sourceTree = "<group>"; };
FDA0FB3428F5EFD90066DEFA /* SwiftUIDayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIDayView.swift; sourceTree = "<group>"; };
Expand All @@ -82,6 +84,7 @@
9381769D249B79CB00E18FA3 /* Demo View Controllers */ = {
isa = PBXGroup;
children = (
9503CCD62F6BFAA400883FC3 /* MonthOverlayDemoViewController.swift */,
938176A6249B85CE00E18FA3 /* DemoViewController.swift */,
939E696E2484CD8E00A8BCC7 /* SingleDaySelectionDemoViewController.swift */,
938176A0249B7CE600E18FA3 /* DayRangeSelectionDemoViewController.swift */,
Expand Down Expand Up @@ -213,6 +216,7 @@
FDA0FB3728F5EFF60066DEFA /* SwiftUIItemModelsDemoViewController.swift in Sources */,
939E696F2484CD8E00A8BCC7 /* SingleDaySelectionDemoViewController.swift in Sources */,
938176A3249B7F0800E18FA3 /* SelectedDayTooltipDemoViewController.swift in Sources */,
9503CCD72F6BFAA400883FC3 /* MonthOverlayDemoViewController.swift in Sources */,
FDA0FB3528F5EFD90066DEFA /* SwiftUIDayView.swift in Sources */,
938176A1249B7CE600E18FA3 /* DayRangeSelectionDemoViewController.swift in Sources */,
FD53899F299476AD007D56EB /* DayRangeSelectionTracker.swift in Sources */,
Expand Down
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
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


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
Expand Up @@ -67,6 +67,7 @@ final class DemoPickerViewController: UIViewController {
("Scroll to Day With Animation", ScrollToDayWithAnimationDemoViewController.self),
("Partial Month Visibility", PartialMonthVisibilityDemoViewController.self),
("Month Grid Background", MonthBackgroundDemoViewController.self),
("Month Days Overlay", MonthOverlayDemoViewController.self),
("SwiftUI Day and Month View", SwiftUIItemModelsDemoViewController.self),
("SwiftUI Screen", SwiftUIScreenDemoViewController.self),
]
Expand All @@ -79,6 +80,7 @@ final class DemoPickerViewController: UIViewController {
("Large Day Range", LargeDayRangeDemoViewController.self),
("Scroll to Day With Animation", ScrollToDayWithAnimationDemoViewController.self),
("Month Grid Background", MonthBackgroundDemoViewController.self),
("Month Days Overlay", MonthOverlayDemoViewController.self),
("SwiftUI Day and Month View", SwiftUIItemModelsDemoViewController.self),
("SwiftUI Screen", SwiftUIScreenDemoViewController.self),
]
Expand Down
15 changes: 15 additions & 0 deletions Sources/Internal/SubviewInsertionIndexTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ final class SubviewInsertionIndexTracker {
index = mainItemsEndIndex
case .daysOfWeekRowSeparator:
index = daysOfWeekRowSeparatorItemsEndIndex
case .monthOverlay:
index = monthOverlayItemsEndIndex
case .overlayItem:
index = overlayItemsEndIndex
case .pinnedDaysOfWeekRowBackground:
Expand All @@ -60,6 +62,7 @@ final class SubviewInsertionIndexTracker {
private var dayRangeItemsEndIndex = 0
private var mainItemsEndIndex = 0
private var daysOfWeekRowSeparatorItemsEndIndex = 0
private var monthOverlayItemsEndIndex = 0
private var overlayItemsEndIndex = 0
private var pinnedDaysOfWeekRowBackgroundEndIndex = 0
private var pinnedDayOfWeekItemsEndIndex = 0
Expand All @@ -72,6 +75,7 @@ final class SubviewInsertionIndexTracker {
dayRangeItemsEndIndex += value
mainItemsEndIndex += value
daysOfWeekRowSeparatorItemsEndIndex += value
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
Expand All @@ -82,6 +86,7 @@ final class SubviewInsertionIndexTracker {
dayRangeItemsEndIndex += value
mainItemsEndIndex += value
daysOfWeekRowSeparatorItemsEndIndex += value
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
Expand All @@ -91,6 +96,7 @@ final class SubviewInsertionIndexTracker {
dayRangeItemsEndIndex += value
mainItemsEndIndex += value
daysOfWeekRowSeparatorItemsEndIndex += value
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
Expand All @@ -99,13 +105,22 @@ final class SubviewInsertionIndexTracker {
case .layoutItemType:
mainItemsEndIndex += value
daysOfWeekRowSeparatorItemsEndIndex += value
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
pinnedDaysOfWeekRowSeparatorEndIndex += value

case .daysOfWeekRowSeparator:
daysOfWeekRowSeparatorItemsEndIndex += value
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
pinnedDaysOfWeekRowSeparatorEndIndex += value

case .monthOverlay:
monthOverlayItemsEndIndex += value
overlayItemsEndIndex += value
pinnedDaysOfWeekRowBackgroundEndIndex += value
pinnedDayOfWeekItemsEndIndex += value
Expand Down
1 change: 1 addition & 0 deletions Sources/Internal/VisibleItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extension VisibleItem {
case pinnedDaysOfWeekRowSeparator
case daysOfWeekRowSeparator(Month)
case dayRange(DayRange)
case monthOverlay(Month)
case overlayItem(OverlaidItemLocation)
}

Expand Down
79 changes: 56 additions & 23 deletions Sources/Internal/VisibleItemsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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]
Expand All @@ -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(
Expand Down Expand Up @@ -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
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!


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)
)
}
}

Expand Down
Loading
Loading