Created
June 23, 2018 11:39
-
-
Save tkirby/b339893abfeb024a4f6a7ad5a3a4d6aa to your computer and use it in GitHub Desktop.
A port of MOScrollView to Swift 4.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MOScrollViewSwift.swift | |
// | |
// Port of MOScrollView (https://github.com/jan-christiansen/MOScrollView) | |
// to swift by Richard Todd Kirby | |
// | |
// | |
import UIKit | |
class MOScrollViewSwift: UIScrollView { | |
static let kDefaultSetContentOffsetDuration: CFTimeInterval = 0.33 | |
/// Constants used for Newton approximation of cubic function root. | |
static let kApproximationTolerance: Double = 0.00000001 | |
static let kMaximumSteps: Int = 10 | |
/// Display link used to trigger event to scroll the view. | |
var displayLink: CADisplayLink? | |
/// Timing function of a scroll animation. | |
var timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) | |
/// Duration of an scroll animation. | |
var duration: CFTimeInterval = 0.0 | |
/// States whether the animation has started. | |
var animationStarted: Bool = false | |
/// Time at the begining of an animation. | |
var beginTime: CFTimeInterval = 0.0 | |
/// The content offset at the begining of an animation. | |
var beginContentOffset:CGPoint = CGPoint.zero | |
/// The delta between the contentOffset at the start of the animation and | |
/// the contentOffset at the end of the animation. | |
var deltaContentOffset:CGPoint = CGPoint.zero | |
func setContentOffset( | |
contentOffset: CGPoint, | |
with timingFunction: CAMediaTimingFunction, | |
duration:CFTimeInterval = kDefaultSetContentOffsetDuration ) | |
{ | |
self.duration = duration | |
self.timingFunction = timingFunction | |
self.deltaContentOffset = CGPointMinus(contentOffset, self.contentOffset) | |
if displayLink == nil | |
{ | |
displayLink = CADisplayLink(target: self, | |
selector: #selector(updateContentOffset(displayLink:))) | |
displayLink?.frameInterval = 1 | |
displayLink?.add(to: RunLoop.current, | |
forMode: RunLoopMode.defaultRunLoopMode) | |
} | |
else | |
{ | |
displayLink?.isPaused = false | |
} | |
} | |
@objc func updateContentOffset(displayLink: CADisplayLink) { | |
if beginTime == 0.0 | |
{ | |
beginTime = displayLink.timestamp | |
beginContentOffset = contentOffset | |
} | |
else | |
{ | |
let deltaTime:CFTimeInterval = displayLink.timestamp - beginTime | |
// Ratio of duration that went by | |
let progress:CGFloat = CGFloat(deltaTime / duration) | |
if progress < 1.0 | |
{ | |
// Ratio adjusted by timing function | |
let adjustedProgress:CGFloat = CGFloat(timingFunctionValue(timingFunction, Double(progress))) | |
if 1 - adjustedProgress < 0.001 | |
{ | |
stopAnimation() | |
} | |
else | |
{ | |
updateProgress(adjustedProgress) | |
} | |
} | |
else | |
{ | |
stopAnimation() | |
} | |
} | |
} | |
private func updateProgress(_ progress: CGFloat) | |
{ | |
let currentDeltaContentOffset:CGPoint = CGPointScalarMult(progress, self.deltaContentOffset) | |
self.contentOffset = CGPointAdd(beginContentOffset, currentDeltaContentOffset) | |
} | |
private func stopAnimation() | |
{ | |
displayLink?.isPaused = true | |
beginTime = 0.0 | |
contentOffset = CGPointAdd(beginContentOffset, deltaContentOffset) | |
delegate?.scrollViewDidEndScrollingAnimation?(self) | |
} | |
private func CGPointScalarMult(_ s: CGFloat, | |
_ p: CGPoint) -> CGPoint | |
{ | |
return CGPoint(x: s * p.x, | |
y: s * p.y) | |
} | |
private func CGPointAdd(_ p: CGPoint, | |
_ q: CGPoint) -> CGPoint | |
{ | |
return CGPoint(x: p.x + q.x, | |
y: p.y + q.y) | |
} | |
private func CGPointMinus(_ p: CGPoint, | |
_ q: CGPoint) -> CGPoint | |
{ | |
return CGPoint(x: p.x - q.x, | |
y: p.y - q.y) | |
} | |
private func cubicFunctionValue(_ a: Double, | |
_ b: Double, | |
_ c: Double, | |
_ d: Double, | |
_ x: Double) -> Double | |
{ | |
return (a*x*x*x)+(b*x*x)+(c*x)+d | |
} | |
private func cubicDerivativeValue(_ a: Double, | |
_ b: Double, | |
_ c: Double, | |
_ d: Double, | |
_ x: Double) -> Double | |
{ | |
/// Derivation of the cubic (a*x*x*x)+(b*x*x)+(c*x)+d | |
return (3*a*x*x)+(2*b*x)+c | |
} | |
private func rootOfCubic(_ a: Double, | |
_ b: Double, | |
_ c: Double, | |
_ d: Double, | |
_ startPoint: Double) -> Double | |
{ | |
// We use 0 as start point as the root will be in the interval [0,1] | |
var x: Double = startPoint | |
var lastX: Double = 1 | |
// Approximate a root by using the Newton-Raphson method | |
var y:Int = 0 | |
while y <= MOScrollViewSwift.kMaximumSteps && | |
fabs(lastX - x) > MOScrollViewSwift.kApproximationTolerance | |
{ | |
lastX = x | |
x = x - (cubicFunctionValue(a, b, c, d, x) / | |
cubicDerivativeValue(a, b, c, d, x)) | |
y += 1 | |
} | |
return x | |
} | |
private func timingFunctionValue(_ function: CAMediaTimingFunction, | |
_ x: Double) -> Double | |
{ | |
var a:[Float] = [0.0, 0.0] | |
var b:[Float] = [0.0, 0.0] | |
var c:[Float] = [0.0, 0.0] | |
var d:[Float] = [0.0, 0.0] | |
function.getControlPoint(at: 0, values: &a) | |
function.getControlPoint(at: 1, values: &b) | |
function.getControlPoint(at: 2, values: &c) | |
function.getControlPoint(at: 3, values: &d) | |
// Look for t value that corresponds to provided x | |
let t:Double = rootOfCubic( | |
Double(-a[0]+3*b[0]-3*c[0]+d[0]), | |
Double(3*a[0]-6*b[0]+3*c[0]), | |
Double(-3*a[0]+3*b[0]), | |
Double(a[0]-Float(x)), x) | |
// Return corresponding y value | |
let y:Double = cubicFunctionValue( | |
Double(-a[1]+3*b[1]-3*c[1]+d[1]), | |
Double(3*a[1]-6*b[1]+3*c[1]), | |
Double(-3*a[1]+3*b[1]), | |
Double(a[1]), t) | |
return y | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment