CAAnimation has an informal 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)
CAAnimation's informal delegate protocol

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?) {}

}
Extending CALayer to include closure-based functions for adding animations

We can create a private class that acts as the animation’s delegate and holds both LayerAnimationBeginClosure and LayerAnimationCompletionClosure:

private class LayerAnimationDelegate: NSObject {
    
    var beginClosure: LayerAnimationBeginClosure?
    var completionClosure: LayerAnimationCompletionClosure?
    
    override func animationDidStart(animation: CAAnimation) {
        guard let beginClosure = beginClosure else { return }
        beginClosure(animation)
    }
    
    override func animationDidStop(animation: CAAnimation, finished: Bool) {
        guard let completionClosure = completionClosure else { return }
        completionClosure(animation, finished)
    }

}
A private proxy class to act as an animation's delegate

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)
    }
}
Implementing the convenience functions in CALayer

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)")
})
Making use of closures to be notified of CALayer animation lifecycle events

Here’s a Gist of the code we went over today, along with a few other conveniences:

  1. 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 why CAAnimation 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.

CALayer