Skip to content

Commit

Permalink
Fix implicit animationg bug (#262)
Browse files Browse the repository at this point in the history
* Fix implicit animationg bug

* Fix pinned days of the week from being rendered off screen

* Update unit tests
  • Loading branch information
bryankeller authored Aug 30, 2023
1 parent 2a71dab commit 937f2a9
Show file tree
Hide file tree
Showing 3 changed files with 420 additions and 49 deletions.
33 changes: 30 additions & 3 deletions Sources/Internal/VisibleItemsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ final class VisibleItemsProvider {
func detailsForVisibleItems(
surroundingPreviouslyVisibleLayoutItem previouslyVisibleLayoutItem: LayoutItem,
offset: CGPoint,
size: CGSize)
isAnimatedUpdatePass: Bool)
-> VisibleItemsDetails
{
// Default the initial capacity to 100, which is approximately enough room for 3 months worth of
Expand All @@ -167,6 +167,13 @@ final class VisibleItemsProvider {
calendarItemModelCache: .init(
minimumCapacity: previousCalendarItemModelCache?.capacity ?? 100))

let bounds: CGRect
if isAnimatedUpdatePass {
bounds = boundsForAnimatedUpdatePass(atOffset: offset)
} else {
bounds = CGRect(origin: offset, size: size)
}

// `extendedBounds` is used to make sure that we're always laying out a continuous set of items,
// even if the last anchor item is completely off screen.
//
Expand All @@ -177,7 +184,6 @@ final class VisibleItemsProvider {
//
// One can think of `extendedBounds`'s purpose as increasing the layout region to compensate
// for extremely fast scrolling / large per-frame bounds differences.
let bounds = CGRect(origin: offset, size: size)
let minX = min(bounds.minX, previouslyVisibleLayoutItem.frame.minX)
let minY = min(bounds.minY, previouslyVisibleLayoutItem.frame.minY)
let maxX = max(bounds.maxX, previouslyVisibleLayoutItem.frame.maxX)
Expand Down Expand Up @@ -242,7 +248,7 @@ final class VisibleItemsProvider {

// Handle pinned day-of-week layout items
if case .vertical(let options) = content.monthsLayout, options.pinDaysOfWeekToTop {
handlePinnedDaysOfWeekIfNeeded(yContentOffset: bounds.minY, context: &context)
handlePinnedDaysOfWeekIfNeeded(yContentOffset: offset.y, context: &context)
}

let visibleDayRange: DayRange?
Expand Down Expand Up @@ -412,6 +418,27 @@ final class VisibleItemsProvider {
return itemOrigin < otherItemOrigin ? item : otherItem
}

private func boundsForAnimatedUpdatePass(atOffset offset: CGPoint) -> CGRect {
// Use a larger bounds (3x the viewport size) if we're in an animated update pass, reducing the
// likelihood of an item popping in / out.
let boundsMultiplier = CGFloat(3)
switch content.monthsLayout {
case .vertical:
return CGRect(
x: offset.x,
y: offset.y - size.height,
width: size.width,
height: size.height * boundsMultiplier)

case .horizontal:
return CGRect(
x: offset.x - size.width,
y: offset.y,
width: size.width * boundsMultiplier,
height: size.height)
}
}

private func monthOrigin(
forMonthContaining layoutItem: LayoutItem,
monthHeaderHeight: CGFloat,
Expand Down
47 changes: 13 additions & 34 deletions Sources/Public/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public final class CalendarView: UIView {

guard isReadyForLayout else { return }

_layoutSubviews(isAnimatedUpdatePass: isInAnimationClosure)
_layoutSubviews(isAnimatedUpdatePass: isAnimatedUpdatePass)
}

/// Sets the content of the `CalendarView`, causing it to re-render, with no animation.
Expand All @@ -222,6 +222,8 @@ public final class CalendarView: UIView {
public func setContent(_ content: CalendarViewContent, animated: Bool) {
let oldContent = self.content

let isInAnimationClosure = UIView.areAnimationsEnabled && UIView.inheritedAnimationDuration > 0

// Do a preparation layout pass with an extended bounds, if we're animating. This ensures that
// views don't pop in if they're animating in from outside the actual bounds.
if animated {
Expand Down Expand Up @@ -284,7 +286,11 @@ public final class CalendarView: UIView {
// If we're animating, force layout with the inherited animation closure or with our own default
// animation. Forcing layout ensures that frame adjustments happen with an animation.
if animated {
let animations = { self.layoutIfNeeded() }
let animations = {
self.isAnimatedUpdatePass = true
self.layoutIfNeeded()
self.isAnimatedUpdatePass = false
}
if isInAnimationClosure {
animations()
} else {
Expand Down Expand Up @@ -514,6 +520,8 @@ public final class CalendarView: UIView {
private var visibleItemsDetails: VisibleItemsDetails?
private var visibleViewsForVisibleItems = [VisibleItem: ItemView]()

private var isAnimatedUpdatePass = false

private var previousBounds = CGRect.zero
private var previousLayoutMargins = UIEdgeInsets.zero

Expand Down Expand Up @@ -552,10 +560,6 @@ public final class CalendarView: UIView {
bounds.size != .zero
}

private var isInAnimationClosure: Bool {
UIView.areAnimationsEnabled && UIView.inheritedAnimationDuration > 0
}

private var scale: CGFloat {
let scale = traitCollection.displayScale
// The documentation mentions that 0 is a possible value, so we guard against this.
Expand Down Expand Up @@ -725,34 +729,10 @@ public final class CalendarView: UIView {
visibleItemsProvider: visibleItemsProvider)
}

// Use an extended bounds (3x the viewport size) if we're in an animated update pass, reducing
// the likelihood of an item popping in / out.
let boundsMultiplier = CGFloat(3)
let offset: CGPoint
let size: CGSize
if isAnimatedUpdatePass {
switch content.monthsLayout {
case .vertical:
offset = CGPoint(
x: scrollView.contentOffset.x,
y: scrollView.contentOffset.y - bounds.height)
size = CGSize(width: bounds.size.width, height: bounds.size.height * boundsMultiplier)

case .horizontal:
offset = CGPoint(
x: scrollView.contentOffset.x - bounds.width,
y: scrollView.contentOffset.y)
size = CGSize(width: bounds.size.width * boundsMultiplier, height: bounds.size.height)
}
} else {
offset = scrollView.contentOffset
size = bounds.size
}

let currentVisibleItemsDetails = visibleItemsProvider.detailsForVisibleItems(
surroundingPreviouslyVisibleLayoutItem: anchorLayoutItem,
offset: offset,
size: size)
offset: scrollView.contentOffset,
isAnimatedUpdatePass: isAnimatedUpdatePass)
self.anchorLayoutItem = currentVisibleItemsDetails.centermostLayoutItem

updateVisibleViews(
Expand Down Expand Up @@ -1138,7 +1118,6 @@ extension CalendarView: WidthDependentIntrinsicContentHeightProviding {
} else {
calendarHeight = bounds.height
}
let size = CGSize(width: calendarWidth, height: calendarHeight)

let visibleItemsProvider = VisibleItemsProvider(
calendar: calendar,
Expand All @@ -1158,7 +1137,7 @@ extension CalendarView: WidthDependentIntrinsicContentHeightProviding {
let visibleItemsDetails = visibleItemsProvider.detailsForVisibleItems(
surroundingPreviouslyVisibleLayoutItem: anchorMonthHeaderLayoutItem,
offset: scrollView.contentOffset,
size: size)
isAnimatedUpdatePass: false)

return CGSize(width: UIView.noIntrinsicMetric, height: visibleItemsDetails.intrinsicHeight)
}
Expand Down
Loading

0 comments on commit 937f2a9

Please sign in to comment.