Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Transfer the decelerating between multiple UIScrollViews
//
// ScrollingDecelerator.swift
// ShopBack
//
// Created by Tai Le on 6/5/20.
// Copyright © 2020 levantAJ. All rights reserved.
//
final class ScrollingDecelerator {
weak var scrollView: UIScrollView?
var scrollingAnimation: TimerAnimationProtocol?
let threshold: CGFloat
init(scrollView: UIScrollView) {
self.scrollView = scrollView
threshold = 0.1
}
}
// MARK: - ScrollingDeceleratorProtocol
extension ScrollingDecelerator: ScrollingDeceleratorProtocol {
func decelerate(by deceleration: ScrollingDeceleration) {
guard let scrollView = scrollView else { return }
let velocity = CGPoint(x: deceleration.velocity.x, y: deceleration.velocity.y * 1000 * threshold)
scrollingAnimation = beginScrollAnimation(initialContentOffset: scrollView.contentOffset, initialVelocity: velocity, decelerationRate: deceleration.decelerationRate.rawValue) { [weak scrollView] point in
guard let scrollView = scrollView else { return }
if deceleration.velocity.y < 0 {
scrollView.contentOffset.y = max(point.y, 0)
} else {
scrollView.contentOffset.y = max(0, min(point.y, scrollView.contentSize.height - scrollView.frame.height))
}
}
}
func invalidateIfNeeded() {
guard scrollView?.isUserInteracted == true else { return }
scrollingAnimation?.invalidate()
scrollingAnimation = nil
}
}
// MARK: - Privates
extension ScrollingDecelerator {
private func beginScrollAnimation(initialContentOffset: CGPoint, initialVelocity: CGPoint,
decelerationRate: CGFloat,
animations: @escaping (CGPoint) -> Void) -> TimerAnimationProtocol {
let timingParameters = ScrollTimingParameters(initialContentOffset: initialContentOffset,
initialVelocity: initialVelocity,
decelerationRate: decelerationRate,
threshold: threshold)
return TimerAnimation(duration: timingParameters.duration, animations: { progress in
let point = timingParameters.point(at: progress * timingParameters.duration)
animations(point)
})
}
}
// MARK: - ScrollTimingParameters
extension ScrollingDecelerator {
struct ScrollTimingParameters {
let initialContentOffset: CGPoint
let initialVelocity: CGPoint
let decelerationRate: CGFloat
let threshold: CGFloat
}
}
extension ScrollingDecelerator.ScrollTimingParameters {
var duration: TimeInterval {
guard decelerationRate < 1
&& decelerationRate > 0
&& initialVelocity.length != 0 else { return 0 }
let dCoeff = 1000 * log(decelerationRate)
return TimeInterval(log(-dCoeff * threshold / initialVelocity.length) / dCoeff)
}
func point(at time: TimeInterval) -> CGPoint {
guard decelerationRate < 1
&& decelerationRate > 0
&& initialVelocity != .zero else { return .zero }
let dCoeff = 1000 * log(decelerationRate)
return initialContentOffset + (pow(decelerationRate, CGFloat(1000 * time)) - 1) / dCoeff * initialVelocity
}
}
// MARK: - TimerAnimation
extension ScrollingDecelerator {
final class TimerAnimation {
typealias Animations = (_ progress: Double) -> Void
typealias Completion = (_ isFinished: Bool) -> Void
weak var displayLink: CADisplayLink?
private(set) var isRunning: Bool
private let duration: TimeInterval
private let animations: Animations
private let completion: Completion?
private let firstFrameTimestamp: CFTimeInterval
init(duration: TimeInterval, animations: @escaping Animations, completion: Completion? = nil) {
self.duration = duration
self.animations = animations
self.completion = completion
firstFrameTimestamp = CACurrentMediaTime()
isRunning = true
let displayLink = CADisplayLink(target: self, selector: #selector(step))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
}
}
// MARK: - TimerAnimationProtocol
extension ScrollingDecelerator.TimerAnimation: TimerAnimationProtocol {
func invalidate() {
guard isRunning else { return }
isRunning = false
stopDisplayLink()
completion?(false)
}
}
// MARK: - Privates
extension ScrollingDecelerator.TimerAnimation {
@objc private func step(displayLink: CADisplayLink) {
guard isRunning else { return }
let elapsed = CACurrentMediaTime() - firstFrameTimestamp
if elapsed >= duration
|| duration == 0 {
animations(1)
isRunning = false
stopDisplayLink()
completion?(true)
} else {
animations(elapsed / duration)
}
}
private func stopDisplayLink() {
displayLink?.isPaused = true
displayLink?.invalidate()
displayLink = nil
}
}
// MARK: - CGPoint
private extension CGPoint {
var length: CGFloat {
return sqrt(x * x + y * y)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func * (lhs: CGFloat, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs * rhs.x, y: lhs * rhs.y)
}
}
final class ScrollingDeceleration {
let velocity: CGPoint
let decelerationRate: UIScrollView.DecelerationRate
init(velocity: CGPoint, decelerationRate: UIScrollView.DecelerationRate) {
self.velocity = velocity
self.decelerationRate = decelerationRate
}
}
// MARK: - Equatable
extension ScrollingDeceleration: Equatable {
static func == (lhs: ScrollingDeceleration, rhs: ScrollingDeceleration) -> Bool {
return lhs.velocity == rhs.velocity
&& lhs.decelerationRate == rhs.decelerationRate
}
}
// MARK: - protocol ScrollingDeceleratorProtocol {
func decelerate(by deceleration: ScrollingDeceleration)
func invalidateIfNeeded()
}
// MARK: - TimerAnimationProtocol
protocol TimerAnimationProtocol {
func invalidate()
}
// MARK: - UIScrollView
extension UIScrollView {
// Indicates that the scrolling is caused by user.
var isUserInteracted: Bool {
return isTracking || isDragging || isDecelerating
}
}
@levantAJ
Copy link
Author

levantAJ commented May 6, 2020

How to use:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let deceleration = ScrollingDeceleration(velocity: velocity, decelerationRate: scrollView.decelerationRate)
    scrollingDecelerator.decelerate(by: deceleration)
}

@jabbarwockeez
Copy link

jabbarwockeez commented Sep 8, 2020

Compile failed, Use of undeclared type TimerAnimationProtocol、CADisplayLinkProtocol、ScrollingDeceleratorProtocol、DateRepositoryProtocol

@levantAJ
Copy link
Author

levantAJ commented Sep 8, 2020

Thanks @jabbarwockeez, updated!

@VikasPrajapatiSA
Copy link

VikasPrajapatiSA commented Oct 5, 2020

DateRepositoryProtocol is missing. Can you please check?

@Frace17
Copy link

Frace17 commented Feb 2, 2021

Thanks for your work, but compile failed. I think you missed DateRepositoryProtocol, can you add it, please?

@levantAJ
Copy link
Author

levantAJ commented Feb 2, 2021

hi @VikasPrajapatiSA @Frace17 the DateRepositoryProtocol was removed and updated as above!

@Frace17
Copy link

Frace17 commented Feb 2, 2021

I still cant achieve the correct behavior of scrollviews, can you provide full example, please?

@eshwavin
Copy link

eshwavin commented Feb 11, 2021

dateRepository is not declared anywhere. Could you please check?

@levantAJ
Copy link
Author

levantAJ commented Feb 17, 2021

Updated the gist @eshwavin

@exera
Copy link

exera commented Feb 22, 2021

UIScrollView has no member isUserInteracted. Can we replace it with scrollView.isTracking ?

@levantAJ
Copy link
Author

levantAJ commented Feb 22, 2021

Just updated the gist @exera

var isUserInteracted: Bool {
        return isTracking || isDragging || isDecelerating
}

Thanks for letting me know !

@huangboju
Copy link

huangboju commented May 1, 2022

Would you please provide a demo ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment