Skip to content
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

Fix implicit animationg bug #262

Merged
merged 3 commits into from
Aug 30, 2023
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
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't want to use the extended bounds for this, otherwise we'll render these pinned items off screen

}

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
Loading