Skip to content

Commit

Permalink
Add animated content update support
Browse files Browse the repository at this point in the history
  • Loading branch information
bryankeller committed Aug 24, 2023
1 parent 38a9eae commit ff9f456
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a `multiDaySelectionDragHandler`, enabling developers to implement multiple-day-selection via a drag gesture (similar to multi-select in the iOS Photos app)
- Added the ability to change the aspect ratio of individual day-of-the-week items
- Added support for self-sizing month headers
- Added a new `setContent(_:animated:)` function, allowing people to perform animated content updates

### Fixed
- Fixed an issue that could cause the calendar to programmatically scroll to a month or day to which it had previously scrolled
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ Features:

- Supports all calendars from `Foundation.Calendar` (Gregorian, Japanese, Hebrew, etc.)
- Display months in a vertically-scrolling or horizontally-scrolling layout
- Declarative API that enables unidirectional data flow for updating the content of the calendar
- Declarative API that encourages unidirectional data flow for updating the content of the calendar
- A custom layout system that enables virtually infinite date ranges without increasing memory usage
- Animated content updates
- Pagination for horizontally-scrolling calendars
- Self-sizing month headers
- Specify custom views (`UIView` or SwiftUI `View`) for individual days, month headers, and days of the week
Expand Down
43 changes: 33 additions & 10 deletions Sources/Public/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,25 +198,35 @@ public final class CalendarView: UIView {

guard isReadyForLayout else { return }

_layoutSubviews(isAnimatedUpdatePass: isAnimatedUpdatePass)

// The layout / update pass has completed, and we can set this back to `false`.
isAnimatedUpdatePass = false
_layoutSubviews(isAnimatedUpdatePass: isInAnimationClosure)
}

/// Sets the content of the `CalendarView`, causing it to re-render.
/// Sets the content of the `CalendarView`, causing it to re-render, with no animation.
///
/// - Parameters:
/// - content: The content to use when rendering `CalendarView`.
public func setContent(_ content: CalendarViewContent) {
setContent(content, animated: false)
}

/// Sets the content of the `CalendarView`, causing it to re-render, with an optional animation.
///
/// If you call this function with `animated` set to `true` in your own animation closure, that animation will be used to perform
/// the content update. If you call this function with `animated` set to `true` outside of an animation closure, a default animation
/// will be used. Calling this function with `animated` set to `false` will result in a non-animated content update, even if you call
/// it from an animation closure.
///
/// - Parameters:
/// - content: The content to use when rendering `CalendarView`.
/// - animated: Whether or not the content update should be animated.
public func setContent(_ content: CalendarViewContent, animated: Bool) {
let oldContent = self.content

// 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.
isAnimatedUpdatePass = UIView.inheritedAnimationDuration > 0 && UIView.areAnimationsEnabled
if isAnimatedUpdatePass {
if animated {
UIView.performWithoutAnimation {
_layoutSubviews(isAnimatedUpdatePass: isAnimatedUpdatePass)
_layoutSubviews(isAnimatedUpdatePass: isInAnimationClosure)
}
}

Expand Down Expand Up @@ -270,6 +280,17 @@ public final class CalendarView: UIView {

self.content = content
setNeedsLayout()

// 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() }
if isInAnimationClosure {
animations()
} else {
UIView.animate(withDuration: 0.3, animations: animations)
}
}
}

/// Returns the accessibility element associated with the specified visible date. If the date is not currently visible, then there will be no
Expand Down Expand Up @@ -493,8 +514,6 @@ 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 @@ -533,6 +552,10 @@ 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
4 changes: 3 additions & 1 deletion Sources/Public/CalendarViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ public struct CalendarViewRepresentable: UIViewRepresentable {
calendarView.didEndDragging = didEndDragging
calendarView.didEndDecelerating = didEndDecelerating

calendarView.setContent(makeContent())
// There's no public API for inheriting the `context.transaction.animation`'s properties here so
// that we can do an equivalent `UIView` animation.
calendarView.setContent(makeContent(), animated: false)
}

// MARK: Fileprivate
Expand Down

0 comments on commit ff9f456

Please sign in to comment.