Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keithnorm/8f3b4d3e2673c1c5e5eefdcd0abd3e9b to your computer and use it in GitHub Desktop.
Save keithnorm/8f3b4d3e2673c1c5e5eefdcd0abd3e9b to your computer and use it in GitHub Desktop.
enable animating a scroll view's contentOffset property with custom easing
//
// UICollectionView+ScrollViewAnimateable.swift
// persimmon
//
// Created by Keith Norman on 5/24/16.
// Copyright © 2016 Good Eggs. All rights reserved.
//
/// Based on https://github.com/plancalculus/MOScrollView. This allows animating contentOffset with a timing function. It's not possible to animate contentOffset via a CAAnimation. It is possible to animate bounds but then scrollview delegate methods don't get called which may be an issue if the animation exposes new cells in a table view or collection view and you expect those cells to render when the scroll into view.
import Foundation
import ObjectiveC
protocol ScrollViewAnimateable {
func setContentOffset(offset: CGPoint, timingFunction: CAMediaTimingFunction, duration: CFTimeInterval)
}
private var displayLinkKey: UInt8 = 0
private var animationStartedKey: UInt8 = 1
private var beginTimeKey: UInt8 = 2
private var beginContentOffsetKey: UInt8 = 3
private var deltaContentOffsetKey: UInt8 = 4
private var durationKey: UInt8 = 5
private var timingFunctionKey: UInt8 = 6
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)
}
extension CAMediaTimingFunction {
public func getControlPoint(index: UInt) -> (x: CGFloat, y: CGFloat)? {
switch index {
case 0...3:
let controlPoint = UnsafeMutablePointer<Float>.alloc(2)
self.getControlPointAtIndex(Int(index), values: controlPoint)
let x: Float = controlPoint[0]
let y: Float = controlPoint[1]
controlPoint.dealloc(2)
return (CGFloat(x), CGFloat(y))
default:
return nil
}
}
public var controlPoints: [CGPoint] {
var controlPoints = [CGPoint]()
for index in 0..<4 {
let controlPoint = UnsafeMutablePointer<Float>.alloc(2)
self.getControlPointAtIndex(Int(index), values: controlPoint)
let x: Float = controlPoint[0]
let y: Float = controlPoint[1]
controlPoint.dealloc(2)
controlPoints.append(CGPoint(x: CGFloat(x), y: CGFloat(y)))
}
return controlPoints
}
func valueAtTime(x: Double) -> Double {
let cp = self.controlPoints
// Look for t value that corresponds to provided x
let a = Double(-cp[0].x+3*cp[1].x-3*cp[2].x+cp[3].x)
let b = Double(3*cp[0].x-6*cp[1].x+3*cp[2].x)
let c = Double(-3*cp[0].x+3*cp[1].x)
let d = Double(cp[0].x)-x
let t = rootOfCubic(a, b, c, d, x)
// Return corresponding y value
let y = cubicFunctionValue(Double(-cp[0].y+3*cp[1].y-3*cp[2].y+cp[3].y), Double(3*cp[0].y-6*cp[1].y+3*cp[2].y), Double(-3*cp[0].y+3*cp[1].y), Double(cp[0].y), t)
return y
}
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 = startPoint
var lastX: Double = 1
let kMaximumSteps = 10
let kApproximationTolerance = 0.00000001
// Approximate a root by using the Newton-Raphson method
var y = 0
while (y <= kMaximumSteps && fabs(lastX - x) > kApproximationTolerance) {
lastX = x
x = x - (cubicFunctionValue(a, b, c, d, x) / cubicDerivativeValue(a, b, c, d, x))
y += 1
}
return x
}
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
}
}
extension UICollectionView: ScrollViewAnimateable {
var displayLink: CADisplayLink? {
get {
return objc_getAssociatedObject(self, &displayLinkKey) as? CADisplayLink
}
set {
objc_setAssociatedObject(self, &displayLinkKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var duration: CFTimeInterval? {
get {
return objc_getAssociatedObject(self, &durationKey) as? CFTimeInterval
}
set {
objc_setAssociatedObject(self, &durationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var timingFunction: CAMediaTimingFunction? {
get {
return objc_getAssociatedObject(self, &timingFunctionKey) as? CAMediaTimingFunction
}
set {
objc_setAssociatedObject(self, &timingFunctionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var animationStarted: CADisplayLink? {
get {
return objc_getAssociatedObject(self, &animationStartedKey) as? CADisplayLink
}
set {
objc_setAssociatedObject(self, &animationStartedKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var beginTime: CFTimeInterval {
get {
return (objc_getAssociatedObject(self, &beginTimeKey) as? CFTimeInterval) ?? 0.0
}
set {
objc_setAssociatedObject(self, &beginTimeKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var beginContentOffset: CGPoint? {
get {
return (objc_getAssociatedObject(self, &beginContentOffsetKey) as? NSValue)?.CGPointValue()
}
set {
let val = NSValue(CGPoint: newValue!)
objc_setAssociatedObject(self, &beginContentOffsetKey, val, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
var deltaContentOffset: CGPoint? {
get {
return (objc_getAssociatedObject(self, &deltaContentOffsetKey) as? NSValue)?.CGPointValue()
}
set {
let val = NSValue(CGPoint: newValue!)
objc_setAssociatedObject(self, &deltaContentOffsetKey, val, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
func setContentOffset(offset: CGPoint, timingFunction: CAMediaTimingFunction, duration: CFTimeInterval) {
self.duration = duration
self.timingFunction = timingFunction
self.deltaContentOffset = CGPointMinus(offset, self.contentOffset)
if (self.displayLink == nil) {
self.displayLink = CADisplayLink(target: self, selector: #selector(updateContentOffset))
self.displayLink!.frameInterval = 1
self.displayLink?.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
} else {
self.displayLink!.paused = false
}
}
func updateContentOffset(displayLink: CADisplayLink) {
if (self.beginTime == 0.0) {
self.beginTime = displayLink.timestamp
self.beginContentOffset = CGPoint(x: -200, y: 0)
} else {
let deltaTime = displayLink.timestamp - self.beginTime
// Ratio of duration that went by
let progress = (CGFloat)(deltaTime / self.duration!)
if (progress < 1.0) {
// Ratio adjusted by timing function
let adjustedProgress = self.timingFunction!.valueAtTime(Double(progress))
self.updateProgress(CGFloat(adjustedProgress))
} else {
self.stopAnimation()
}
}
}
private func updateProgress(progress: CGFloat) {
let currentDeltaContentOffset = CGPointScalarMult(progress, self.deltaContentOffset!)
self.contentOffset = CGPointAdd(self.beginContentOffset!, currentDeltaContentOffset)
}
private func stopAnimation() {
self.displayLink?.paused = true
self.beginTime = 0.0
self.contentOffset = CGPointAdd(self.beginContentOffset!, self.deltaContentOffset!)
self.delegate?.scrollViewDidEndScrollingAnimation?(self)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment