How to identify CAAnimation within the animationDidStop delegate?
Asked Answered
K

10

110

I had a problem where I had a series of overlapping CATransition / CAAnimation sequences, all of which I needed to perform custom operations when the animations stopped, but I only wanted one delegate handler for animationDidStop.

However, I had a problem, there didn't appear to be a way to uniquely identify each CATransition / CAAnimation in the animationDidStop delegate.

I solved this problem via the key / value system exposed as part of CAAnimation.

When you start your animation use the setValue method on the CATransition / CAAnimation to set your identifiers and values to use when animationDidStop fires:

-(void)volumeControlFadeToOrange
{   
    CATransition* volumeControlAnimation = [CATransition animation];
    [volumeControlAnimation setType:kCATransitionFade];
    [volumeControlAnimation setSubtype:kCATransitionFromTop];
    [volumeControlAnimation setDelegate:self];
    [volumeControlLevel setBackgroundImage:[UIImage imageNamed:@"SpecialVolume1.png"] forState:UIControlStateNormal];
    volumeControlLevel.enabled = true;
    [volumeControlAnimation setDuration:0.7];
    [volumeControlAnimation setValue:@"Special1" forKey:@"MyAnimationType"];
    [[volumeControlLevel layer] addAnimation:volumeControlAnimation forKey:nil];    
}

- (void)throbUp
{
    doThrobUp = true;

    CATransition *animation = [CATransition animation]; 
    [animation setType:kCATransitionFade];
    [animation setSubtype:kCATransitionFromTop];
    [animation setDelegate:self];
    [hearingAidHalo setBackgroundImage:[UIImage imageNamed:@"m13_grayglow.png"] forState:UIControlStateNormal];
    [animation setDuration:2.0];
    [animation setValue:@"Throb" forKey:@"MyAnimationType"];
    [[hearingAidHalo layer] addAnimation:animation forKey:nil];
}

In your animationDidStop delegate:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag{

    NSString* value = [theAnimation valueForKey:@"MyAnimationType"];
    if ([value isEqualToString:@"Throb"])
    {
       //... Your code here ...
       return;
    }


    if ([value isEqualToString:@"Special1"])
    {
       //... Your code here ...
       return;
    }

    //Add any future keyed animation operations when the animations are stopped.
 }

The other aspect of this is that it allows you to keep state in the key value pairing system instead of having to store it in your delegate class. The less code, the better.

Be sure to check out the Apple Reference on Key Value Pair Coding.

Are there better techniques for CAAnimation / CATransition identification in the animationDidStop delegate?

Thanks, --Batgar

Kiker answered 10/8, 2009 at 14:4 Comment(2)
Batgar, When I googled for "iphone animationDidStop identify", the first hit was your post, suggesting the use of key-value to identify the animation. Just what I needed, thank you. RudiTeen
Be aware that CAAnimation's delegate is strong, so you might need to set it to nil to avoid retain cycles!Noisome
B
93

Batgar's technique is too complicated. Why not take advantage of the forKey parameter in addAnimation? It was intended for this very purpose. Just take out the call to setValue and move the key string to the addAnimation call. For example:

[[hearingAidHalo layer] addAnimation:animation forKey:@"Throb"];

Then, in your animationDidStop callback, you can do something like:

if (theAnimation == [[hearingAidHalo layer] animationForKey:@"Throb"]) ...
Belike answered 7/9, 2009 at 21:14 Comment(6)
I'd like to mention that using the above INCREMENTS THE RETAIN COUNT! Be warned. That is, animationForKey: increments the retain count of your CAAnimation object.Kamin
@Kamin That is not very surprsising, is it? By adding an animation to a layer, the layer is owning the animation, so the animation's retain count is of course incremented.Flagstaff
Doesn't work - by the time the stop selector is called, the animation no longer exists. You get a null referrence.Electrotype
That's a misuse of the forKey: parameter, and there's no need for it. What Batgar was doing is exactly right - key-value coding allows you to attach any arbitrary data to your animation, so you can easily identify it.Copperhead
Adam, see jimt’s answer below – you must set anim.removedOnCompletion = NO; so that it still exists when -animationDidStop:finished: is called.Paloma
Do we have to remove it after? To free up memory?Antibes
J
46

I just came up with an even better way to do completion code for CAAnimations:

I created a typedef for a block:

typedef void (^animationCompletionBlock)(void);

And a key that I use to add a block to an animation:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Then, if I want to run animation completion code after a CAAnimation finishes, I set myself as the delegate of the animation, and add a block of code to the animation using setValue:forKey:

animationCompletionBlock theBlock = ^void(void)
{
  //Code to execute after the animation completes goes here    
};
[theAnimation setValue: theBlock forKey: kAnimationCompletionBlock];

Then, I implement an animationDidStop:finished: method, that checks for a block at the specified key and executes it if found:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
  animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock];
  if (theBlock)
    theBlock();
}

The beauty of this approach is that you can write the cleanup code in the same place where you create the animation object. Better still, since the code is a block, it has access to local variables in the enclosing scope in which it's defined. You don't have to mess with setting up userInfo dictionaries or other such nonsense, and don't have to write an ever-growing animationDidStop:finished: method that gets more and more complex as you add different kinds of animations.

Truth be told, CAAnimation should have a completion block property built into it, and system support for calling it automatically if one is specified. However, the above code gives you that same functionality with only a few lines of extra code.

Javelin answered 5/1, 2012 at 19:24 Comment(7)
Someone also put together a category on CAAnimation for this: github.com/xissburg/CAAnimationBlocksSieve
This doesn't seem to be right. Quite often, I get an EXEC_Err right after theBlock(); is invoked, and I believe it is due to the fact that the scope of the block was destroyed.Straley
I've been using the block for a while, and it works MUCH better than Apple's terrible "official" approach.Electrotype
@mahboudz, I've used this approach on several projects and it works flawlessly. You can download a working example project from github at github.com/DuncanMC/iOS-CAAnimation-group-demo that shows this approach in running code. The animation object retains the block when you add it with setVAlue:forKey:, and blocks do various kinds of magic to keep their enclosing scope alive while they are alive. (Sometimes copying objects from the stack to the heap, sometimes retaining objects, etc.) I would have to go look it up to explain the specifics, but it works.Javelin
I'm fairly sure that you'd need a to [block copy] that block before setting it as a value for a property.Frisk
No, you don't need to copy the block.Javelin
Great solution! Awesome!Amphibology
M
36

All other answers are way too complicated! Why don't you just add your own key to identify the animation?

This solution is very easy all you need is to add your own key to the animation (animationID in this example)

Insert this line to identify animation1:

[myAnimation1 setValue:@"animation1" forKey:@"animationID"];

and this to identify animation2:

[myAnimation2 setValue:@"animation2" forKey:@"animationID"];

Test it like this:

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
    if([[animation valueForKey:@"animationID"] isEqual:@"animation1"]) {
    //animation is animation1

    } else if([[animation valueForKey:@"animationID"] isEqual:@"animation2"]) {
    //animation is animation2

    } else {
    //something else
    }
}

It does not require any instance variables:

Mount answered 21/9, 2013 at 4:48 Comment(1)
I am getting some int value (int(0)) in animationDidStop as [animation valueForKey:@"animationID"]Antihalation
F
33

The second approach will only work if you explicitly set your animation to not be removed on completion before running it:

CAAnimation *anim = ...
anim.removedOnCompletion = NO;

If you fail to do so, your animation will get removed before when it completes, and the callback will not find it in the dictionary.

Freddyfredek answered 7/7, 2010 at 5:17 Comment(3)
This should be a comment, not an answer.Sogdiana
I wonder if it's necessary to remove it explicitly afterwards with removeAnimationForKey ?Trimetrogon
It really depends what you want to do. You could removed it if necessary or leave it because you want do something else in tandem.Brogdon
S
15

To make explicit what's implied from above (and what brought me here after a few wasted hours): don't expect to see the original animation object that you allocated passed back to you by

 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)flag 

when the animation finishes, because [CALayer addAnimation:forKey:] makes a copy of your animation.

What you can rely on, is that the keyed values you gave to your animation object are still there with equivalent value (but not necessarily pointer equivalence) in the replica animation object passed with the animationDidStop:finished: message. As mentioned above, use KVC and you get ample scope to store and retrieve state.

Slogan answered 15/2, 2011 at 22:30 Comment(4)
+1 This is the best solution! You can set the 'name' of the animation with [animation setValue:@"myanim" forKey:@"name"] and you can even set the layer being animated using [animation setValue:layer forKey:@"layer"]. These values can then be retrieved within the delegate methods.Billie
valueForKey: returns nil for me, any idea why?Noisome
@IulianOnofrei check that your animation wasn't displaced by another animation for the same property — can happen as unexpected side effect.Slogan
@t0rst, Sorry, having multiple animations and using copy paste, I was setting different values on the same animation variable.Noisome
P
2

I can see mostly objc answers I will make one for swift 2.3 based on the best answer above.

For a start it will be good to store all those keys on a private struct so it is type safe and changing it in the future won't bring you annoying bugs just because you forgot to change it everywhere in the code:

private struct AnimationKeys {
    static let animationType = "animationType"
    static let volumeControl = "volumeControl"
    static let throbUp = "throbUp"
}

As you can see I have changed the names of the variables/animations so it is more clear. Now setting these keys when the animation is created.

volumeControlAnimation.setValue(AnimationKeys.volumeControl, forKey: AnimationKeys.animationType)

(...)

throbUpAnimation.setValue(AnimationKeys.throbUp, forKey: AnimationKeys.animationType)

Then finally handling the delegate for when the animation stops

override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    if let value = anim.valueForKey(AnimationKeys.animationType) as? String {
        if value == AnimationKeys.volumeControl {
            //Do volumeControl handling
        } else if value == AnimationKeys.throbUp {
            //Do throbUp handling
        }
    }
}
Pneumatics answered 17/1, 2017 at 10:31 Comment(0)
A
1

Xcode 9 Swift 4.0

You can use Key Values to relate an animation you added to the animation returned in animationDidStop delegate method.

Declare a dictionary to contain all active animations and related completions:

 var animationId: Int = 1
 var animating: [Int : () -> Void] = [:]

When you add your animation, set a key for it:

moveAndResizeAnimation.setValue(animationId, forKey: "CompletionId")
animating[animationId] = {
    print("completion of moveAndResize animation")
}
animationId += 1    

In animationDidStop, the magic happens:

    let animObject = anim as NSObject
    if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
        if let completion = animating.removeValue(forKey: keyValue) {
            completion()
        }
    }
Actaeon answered 3/10, 2017 at 23:38 Comment(0)
W
0

IMHO using Apple's key-value is the elegant way of doing this: it's specifically meant to allow adding application specific data to objects.

Other much less elegant possibility is to store references to your animation objects and do a pointer comparision to identify them.

Wardieu answered 10/8, 2009 at 14:55 Comment(1)
This will never work - you cannot do pointer equivalence, because Apple changes the pointer.Electrotype
P
0

For me to check if 2 CABasicAnimation object are the same animation, I use keyPath function to do exactly as that.

if([animationA keyPath] == [animationB keyPath])

  • There are no need to set KeyPath for CABasicAnimation as it will no longer animate
Peraea answered 31/8, 2012 at 4:59 Comment(1)
the question relates to delegate callbacks, and keyPath is not a method on CAAnimationPhototopography
L
0

I like to use setValue:forKey: to keep a reference of the view I'm animating, it's more safe than trying to uniquely identify the animation based on ID because the same kind of animation can be added to different layers.

These two are equivalent:

[UIView animateWithDuration: 0.35
                 animations: ^{
                     myLabel.alpha = 0;
                 } completion: ^(BOOL finished) {
                     [myLabel removeFromSuperview];
                 }];

with this one:

CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @([myLabel.layer opacity]);
fadeOut.toValue = @(0.0);
fadeOut.duration = 0.35;
fadeOut.fillMode = kCAFillModeForwards;
[fadeOut setValue:myLabel forKey:@"item"]; // Keep a reference to myLabel
fadeOut.delegate = self;
[myLabel.layer addAnimation:fadeOut forKey:@"fadeOut"];
myLabel.layer.opacity = 0;

and in the delegate method:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    id item = [anim valueForKey:@"item"];

    if ([item isKindOfClass:[UIView class]])
    {
        // Here you can identify the view by tag, class type 
        // or simply compare it with a member object

        [(UIView *)item removeFromSuperview];
    }
}
Leda answered 2/4, 2015 at 11:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.