-
-
Save levantAJ/7860d3053324e4fc20e12d5dd3c51fb1 to your computer and use it in GitHub Desktop.
| // | |
| // 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 | |
| } | |
| } |
Compile failed, Use of undeclared type TimerAnimationProtocol、CADisplayLinkProtocol、ScrollingDeceleratorProtocol、DateRepositoryProtocol
Thanks @jabbarwockeez, updated!
DateRepositoryProtocol is missing. Can you please check?
Thanks for your work, but compile failed. I think you missed DateRepositoryProtocol, can you add it, please?
hi @VikasPrajapatiSA @Frace17 the DateRepositoryProtocol was removed and updated as above!
I still cant achieve the correct behavior of scrollviews, can you provide full example, please?
dateRepository is not declared anywhere. Could you please check?
Updated the gist @eshwavin
UIScrollView has no member isUserInteracted. Can we replace it with scrollView.isTracking ?
Just updated the gist @exera
var isUserInteracted: Bool {
return isTracking || isDragging || isDecelerating
}
Thanks for letting me know !
Would you please provide a demo ?
Hello
Please replace line 188 with this :
// MARK: - ScrollingDeceleratorProtocol
protocol ScrollingDeceleratorProtocol {
How to use: