Skip to content

Instantly share code, notes, and snippets.

@tkirby
Created June 23, 2018 11:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkirby/b339893abfeb024a4f6a7ad5a3a4d6aa to your computer and use it in GitHub Desktop.
Save tkirby/b339893abfeb024a4f6a7ad5a3a4d6aa to your computer and use it in GitHub Desktop.
A port of MOScrollView to Swift 4.1
//
// 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