How to remove a layer when its animation completes?
Asked Answered
L

4

6

I am making an iOS App. I have several CALayer objects that eventually will be deleted by a (shrinking) animation. When the animation is completed, and animationDidStop:finished is invoked, I would like to remove the CALayer object from the super view and delete it.

  1. But how can I get the CALayer object in animationDidStop:finished? I would have guessed that the CAanimation-object had a pointer to the layer, but I can't find it in the doc.
  2. Is there a better way to handle the issue? (Actually, I have several animation objects added to the same layer, and, ideally, I would like to remove the layer only when the last animation completes)
Ludwick answered 16/7, 2013 at 23:6 Comment(0)
G
1

When you create the animation and set the delegate, just pass the CALayer you want to remove with your animation.

As for removing all the animations, you have two options:

  1. You can check your CALayer's animationKeys for any existing animations.
  2. You can use a CAAnimationGroup and group all your animations together.
Gilda answered 16/7, 2013 at 23:20 Comment(5)
I am invoking addAnimation:forKey:, how can I pass a CALayer object?Ludwick
You could have a NSMutableArray property on your delegate. Any time you use addAnimation:forKey: add that CALayer to the NSMutableArray. Then you can reference all the CALayers from your delegate.Gilda
So you mean that animationDidStop:finished: should scan through that mutable array to see if it can find any layer objects that has no ongoing animations, and then remove them from both the array and the super layer?Ludwick
Yes, or if all your animations are on a single CALayer you can skip the NSMutableArray. CAAnimations are pretty low level. They don't take car of much other than animating. Thats why the UIView block animations are more common.Gilda
I have posted an alternative solution, but I check your solution as the answer!Ludwick
D
10

It's been a long time since this question has been answered, however i'll try and add a more swifty solution, in case anybody is still looking for a more clean solution nowadays.

If everything you're interested in is simply remove the layer as soon as the CAAnimation is finished, you can assign the animation's delegate to a simple NSObject that holds a reference to the target layer and waits for the animation callback in order to dismiss it.

Let's call this helper object LayerRemover:

class LayerRemover: NSObject, CAAnimationDelegate {
    private weak var layer: CALayer?

    init(for layer: CALayer) {
        self.layer = layer
        super.init()
    }

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        layer?.removeFromSuperlayer()
    }
}

Everything this object does is receiving a CALayer reference via initializer and waiting for the animationDidStop callback before removing the layer. At this point, as soon as the CAAnimation that's retaining it via the delegate property is deinitialized, the Layer remover gets purged too.

Now, all you have to do is actually instatiate this remover and use it:

let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 200, height: 100)).cgPath
let myAnimation = CABasicAnimation(keyPath: "strokeEnd")
...
myAnimation.delegate = LayerRemover(for: layer)

That's it!

Please note that you don't have to retain any reference to the LayerRemover object since from the Apple documentation we can read that

The delegate object is retained by the receiver. This is a rare exception to the memory management rules described in Advanced Memory Management Programming Guide.

Dog answered 20/6, 2018 at 12:40 Comment(2)
Love this solution! Also appreciate the extra attention to details regarding memory management.Pacha
That's a really clean solution, much better than tracking every animation in animationDidStop(:finished:). Thank you! 🙂Humbertohumble
G
1

When you create the animation and set the delegate, just pass the CALayer you want to remove with your animation.

As for removing all the animations, you have two options:

  1. You can check your CALayer's animationKeys for any existing animations.
  2. You can use a CAAnimationGroup and group all your animations together.
Gilda answered 16/7, 2013 at 23:20 Comment(5)
I am invoking addAnimation:forKey:, how can I pass a CALayer object?Ludwick
You could have a NSMutableArray property on your delegate. Any time you use addAnimation:forKey: add that CALayer to the NSMutableArray. Then you can reference all the CALayers from your delegate.Gilda
So you mean that animationDidStop:finished: should scan through that mutable array to see if it can find any layer objects that has no ongoing animations, and then remove them from both the array and the super layer?Ludwick
Yes, or if all your animations are on a single CALayer you can skip the NSMutableArray. CAAnimations are pretty low level. They don't take car of much other than animating. Thats why the UIView block animations are more common.Gilda
I have posted an alternative solution, but I check your solution as the answer!Ludwick
F
0

See if this answer helps: Perform an action after the animation has finished I find animateWithDuration:animations:completion: to be way easier to use than working directly with CALayer. You can chain multiple animations via the completion handler and then remove the layer in the last one. eg:

[UIView animateWithDuration:1.0 animations:^{
    // do first animation in the sequence
} completion:^(BOOL finished) {
    [UIView animateWithDuration:1.0 animations:^{
        // do second animation in the sequence
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:1.0 animations:^{
            // do third animation in the sequence
        } completion:^(BOOL finished) {
            // remove layer after all are done
        }];
    }];
}];

Possibly a little messy this way, but you could refactor these into their own method calls, for instance.

Ferret answered 16/7, 2013 at 23:19 Comment(1)
I am not sure how to use blocks to animate CALayer objects (in ioS5 or 6).Ludwick
L
0

One alternativ solution is to add a layer pointer to the animation object's dictionary, as follows

// in some define section
#define kAnimationRemoveLayer @"animationRemoveLayer"

then, in the animationDidStop,

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag{
    CALayer *lay = [theAnimation valueForKey:kAnimationRemoveLayer];
    if(lay){
        [lay removeAllAnimations];
        [lay removeFromSuperlayer];
    }
}

and, finally, in the animation setup,

CALAyer * lay = ... ;
BOOL    shouldRemove = .... ; 
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.delegate = self;
if (shouldRemove)
    [anim setValue:lay forKey:kAnimationRemoveLayer];
Ludwick answered 8/8, 2013 at 16:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.