How to identify CAAnimation within the animationDidStop delegate?

IphoneCore Animation

Iphone Problem Overview


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

Iphone Solutions


Solution 1 - Iphone

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"]) ...

Solution 2 - Iphone

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.

Solution 3 - Iphone

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.

Solution 4 - Iphone

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:

Solution 5 - Iphone

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.

Solution 6 - Iphone

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
        }
    }
}

Solution 7 - Iphone

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()
        }
    }

Solution 8 - Iphone

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.

Solution 9 - Iphone

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

Solution 10 - Iphone

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];
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBatgarView Question on Stackoverflow
Solution 1 - IphonevocaroView Answer on Stackoverflow
Solution 2 - IphoneDuncan CView Answer on Stackoverflow
Solution 3 - IphonejimtView Answer on Stackoverflow
Solution 4 - IphoneTibidaboView Answer on Stackoverflow
Solution 5 - Iphonet0rstView Answer on Stackoverflow
Solution 6 - IphoneapinhoView Answer on Stackoverflow
Solution 7 - IphoneEng YewView Answer on Stackoverflow
Solution 8 - IphoneTeemu KurppaView Answer on Stackoverflow
Solution 9 - IphoneSirisilp KongsilpView Answer on Stackoverflow
Solution 10 - IphoneAndrei MarincasView Answer on Stackoverflow