Quick Bits: Closure-based CAAnimationDelegate
CAAnimation
has an informal a formal—as of iOS 10—delegate protocol for informing an object when an animation starts or stops, which is useful in many cases. However, with the advent of blocks and closures, using delegates for this type of thing can be cumbersome, especially if you are managing several animations. UIKit introduced convenient, block-based animation methods, yet Core Animation never did the same. Let’s fix that.
CAAnimation
has two functions for responding to animation events: starting and stopping. CAAnimation
calls those functions on its delegate, if it exists, during its running lifecycle.
func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
Because animation objects are added to layers, we can create a CALayer
extension that declares a couple of convenience functions for adding animations and includes lifecycle closures:
extension CALayer {
func addAnimation(_ animation: CAAnimation, forKey key: String?, completionClosure: LayerAnimationCompletionClosure?) {}
func addAnimation(_ animation: CAAnimation, forKey key: String?, beginClosure: LayerAnimationBeginClosure?, completionClosure: LayerAnimationCompletionClosure?) {}
}
We can create a private class that acts as the animation’s delegate and holds both LayerAnimationBeginClosure
and LayerAnimationCompletionClosure
:
fileprivate class LayerAnimationDelegate: NSObject {
var beginClosure: LayerAnimationBeginClosure?
var completionClosure: LayerAnimationCompletionClosure?
}
extension LayerAnimationDelegate (CAAnimationDelegate) {
func animationDidStart(animation: CAAnimation) {
guard let beginClosure = beginClosure else { return }
beginClosure(animation)
}
func animationDidStop(animation: CAAnimation, finished: Bool) {
guard let completionClosure = completionClosure else { return }
completionClosure(animation, finished)
}
}
Then we can implement the CALayer
extension functions we declared above: 1
extension CALayer {
func addAnimation(_ animation: CAAnimation, forKey key: String?, completionClosure: LayerAnimationCompletionClosure?) {
addAnimation(animation, forKey: key, beginClosure: nil, completionClosure: completionClosure)
}
func addAnimation(_ animation: CAAnimation, forKey key: String?, beginClosure: LayerAnimationBeginClosure?, completionClosure: LayerAnimationCompletionClosure?) {
let animationDelegate = LayerAnimationDelegate()
animationDelegate.beginClosure = beginClosure
animationDelegate.completionClosure = completionClosure
animation.delegate = animationDelegate
addAnimation(animation, forKey: key)
}
}
Now we have convenient, closure-based access to CAAnimation
’s lifecycle functions when we add CALayer
animations:
layer.addAnimation(animation, forKey: "positionXAnimation", beginClosure: { animation in
print("My animation (\(animation)) began.")
}, completionClosure: { (animation, finished) in
print("My animation (\(animation)) completed. Did it finish? \(finished)")
})
Here’s a Gist of the code we went over today, along with a few other conveniences:
-
CAAnimation
actually holds a strong reference to its delegate per the documentation, so the animation delegate class we initialize will stick around long enough to do its job. In fact, that is the reason whyCAAnimation
breaks the conventional memory management rules for delegate objects: if it was weak, the animation wouldn’t be able to guarantee that its delegate still existed after a potentially lengthy duration. ↩