Last active
July 19, 2019 11:50
-
-
Save ChrisLawther/a0125487e3c2339bb80d513634c29b4e to your computer and use it in GitHub Desktop.
A cleaner way to write chained UIView animations
This file contains 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
// Chaining UIView automations inside completion blocks of earlier animations quickly gets confusing. | |
// Breaking animations out into their own function can ease this a little. | |
// Here we take an alternative approach which allows chained animations to be written more naturally. | |
// There's more repetition than I'd like, to support the two flavours of animation, but it makes for | |
// a cleaner call site. | |
// (with or without initial velocity and damping). | |
// | |
// (It's probably been done before by others, but XCode 10 just landed and I wanted something to ) | |
// (do in a Playground to see if they're any better behaved. ) | |
// See usage example below | |
class ChainingAnimator { | |
typealias AnimationBlock = () -> Void | |
var phases = [AnimationPhase]() | |
// To match the two corresponding versions of UIView.animate() | |
enum AnimationPhase { | |
case simple(duration: TimeInterval, | |
delay:TimeInterval, | |
block: AnimationBlock, | |
options: UIView.AnimationOptions) | |
case springy(duration: TimeInterval, | |
delay:TimeInterval, | |
damping: CGFloat, | |
velocity: CGFloat, | |
block: AnimationBlock, | |
options: UIView.AnimationOptions) | |
} | |
// The simple case, with no initial velocity or damping | |
func then( | |
duration: TimeInterval, | |
delay: TimeInterval = 0, | |
options: UIView.AnimationOptions = [], | |
block: @escaping AnimationBlock | |
) -> ChainingAnimator { | |
phases.append( | |
.simple(duration: duration, | |
delay: delay, | |
block: block, | |
options: options | |
) | |
) | |
return self | |
} | |
// A version with initial velocity and damping | |
func then( | |
duration: TimeInterval, | |
delay: TimeInterval = 0, | |
damping: CGFloat, | |
velocity: CGFloat, | |
options: UIView.AnimationOptions = [], | |
block: @escaping AnimationBlock | |
) -> ChainingAnimator { | |
phases.append( | |
.springy(duration: duration, | |
delay: delay, | |
damping: damping, | |
velocity: velocity, | |
block: block, | |
options: options | |
) | |
) | |
return self | |
} | |
// Called to complete and initiate the chain | |
func run(then onComplete: @escaping () -> Void = {}) { | |
let animationSequence = phases.reversed().reduce(onComplete) { completion, phase in | |
phase.block(withCompletion: completion) | |
} | |
animationSequence() | |
} | |
} | |
extension ChainingAnimator.AnimationPhase { | |
func block(withCompletion completion: @escaping ()->Void) -> ()->Void { | |
switch self { | |
case .simple(let duration, let delay, let block, let options): | |
return { | |
UIView.animate(withDuration: duration, | |
delay: delay, | |
options: options, | |
animations: block, | |
completion: { _ in completion() }) | |
} | |
case .springy(let duration, let delay, let damping, let velocity, let block, let options): | |
return { | |
UIView.animate(withDuration: duration, | |
delay: delay, | |
usingSpringWithDamping: damping, | |
initialSpringVelocity: velocity, | |
options: options, | |
animations: block, | |
completion: { _ in completion() }) | |
} | |
} | |
} | |
} | |
// These two free functions only serve to hide the initial `ChainingAnimator().then(...` | |
// from the caller. That's only a very small win though. | |
func animate(duration: TimeInterval, | |
delay: TimeInterval = 0, | |
options: UIView.AnimationOptions = [], | |
block: @escaping ChainingAnimator.AnimationBlock) -> ChainingAnimator | |
{ | |
return ChainingAnimator().then(duration: duration, | |
delay: delay, | |
options: options, | |
block: block) | |
} | |
func animate(duration: TimeInterval, | |
delay: TimeInterval = 0, | |
damping: CGFloat, | |
velocity: CGFloat, | |
options: UIView.AnimationOptions = [], | |
block: @escaping ChainingAnimator.AnimationBlock) -> ChainingAnimator | |
{ | |
return ChainingAnimator().then(duration: duration, | |
delay: delay, | |
damping: damping, | |
velocity: velocity, | |
options: options, | |
block: block) | |
} | |
// Usage starts with a call to `animate(duration:delay:block:)`, which | |
// returns an object on which we can call `.then(duration:delay:block)` | |
// which returns another instance of the same object, allowing chains | |
// of arbitrary length. Once the chain is complete, we call `.run()` on | |
// the final object. | |
animate(duration: 1) { | |
// Rotate a quarter turn to the right | |
someView.transform = CGAffineTransform(rotationAngle: .pi / 2) | |
}.then(duration: 2, delay: 1) { | |
// After a 1s delay, double in size, maintaining the rotation | |
someView.transform = someView.transform.scaledBy(x:2, y: 2) | |
}.then(duration: 1) { | |
// Revert to original size and orientation | |
someView.transform = .identity | |
}.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment