Skip to content

Instantly share code, notes, and snippets.

@ChrisLawther
Last active July 19, 2019 11:50
Show Gist options
  • Save ChrisLawther/a0125487e3c2339bb80d513634c29b4e to your computer and use it in GitHub Desktop.
Save ChrisLawther/a0125487e3c2339bb80d513634c29b4e to your computer and use it in GitHub Desktop.
A cleaner way to write chained UIView animations
// 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