Upcoming and OnDemand Webinars View full list

Core Animation Target-Action Layers

Joe Conway

When you write a Core Animation heavy application, you spend a lot of time implementing code that executes when an animation finishes. Typically, the view controller whose view contains the animating layers implements animationDidStop:finished: and does a series of checks to see which animation finished. This method becomes difficult to manage as the number of animations it handles increases. You must also set the delegate of each animation object and tell them not to remove themselves when finished. You end up writing a lot of code over and over again.

It would be simpler to have animations work more like UIControls. A layer would have a set of target-action pairs that would be triggered when an animation it is running completes. That way, you could easily chain animations or have code executed only after an animation completes.

How would you achieve this functionality? You would subclass CALayer. This layer subclass would have a list of target-action pairs for animation keys. (Note: Not animation key paths, but rather the name you assign to an animation when it is added to a layer.) Here is the code for that subclass:

BNRActionLayer.h:
`

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface BNRActionLayer : CALayer {
    NSMutableDictionary *targetActionPairs;
}
- (void)addTarget:(id)t action:(SEL)a forKey:(NSString *)k;
- (void)removeTarget:(id)t action:(SEL)a forKey:(NSString *)k;
- (NSArray *)actionsForTarget:(id)t forKey:(NSString *)k;
@end

`

BNRActionLayer.m:
`

#import "BNRActionLayer.h"

// Declare a private class to keep track of target-action pairs
@interface BNRActionLayerTargetActionPair : NSObject
{
    id target;
    SEL action;
}
@property (nonatomic, assign) id target;
@property (nonatomic, assign) SEL action;
@end

@implementation BNRActionLayerTargetActionPair
@synthesize target, action;
@end

@interface BNRActionLayer (Private)
- (NSMutableArray *)pairsForKey:(NSString *)k;
@end

@implementation BNRActionLayer
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
    for(NSString *observedKey in targetActionPairs) {
        if([self animationForKey:observedKey] == theAnimation) {
            NSArray *pairs = [self pairsForKey:observedKey];
            [self removeAnimationForKey:observedKey];
            for(BNRActionLayerTargetActionPair *pair in pairs) {
                if(flag)
                    [[pair target] performSelector:[pair action] 
                                        withObject:self];
            }
        }
    }
}
- (void)addAnimation:(CAAnimation *)theAnimation forKey:(NSString *)key
{
    NSArray *targetActionsForThisKey = [targetActionPairs objectForKey:key];
    if([targetActionsForThisKey count] > 0) {
        [theAnimation setRemovedOnCompletion:NO];
        [theAnimation setDelegate:self];
    } 		
    [super addAnimation:theAnimation forKey:key];
}
- (void)addTarget:(id)t action:(SEL)a forKey:(NSString *)k
{
    if(!targetActionPairs)
        targetActionPairs = [[NSMutableDictionary alloc] init];

    NSMutableArray *pairsForKey = [self pairsForKey:k];
    if(!pairsForKey) {
        pairsForKey = [NSMutableArray array];
        [targetActionPairs setObject:pairsForKey forKey:k];
    }
	
    for(BNRActionLayerTargetActionPair *pair in pairsForKey) {
        if([pair target] == t && [pair action] == a)
        return;
    }
	
    BNRActionLayerTargetActionPair *newPair = 
        [[BNRActionLayerTargetActionPair alloc] init];
    [newPair setTarget:t];
    [newPair setAction:a];
    [pairsForKey addObject:newPair];
    [newPair release];
}
- (void)removeTarget:(id)t action:(SEL)a forKey:(NSString *)k
{
    NSMutableArray *pairsForKey = [self pairsForKey:k];
    if(!pairsForKey)
		return;
		
    BNRActionLayerTargetActionPair *removablePair = nil;
    for(BNRActionLayerTargetActionPair *pair in pairsForKey) {
        if([pair target] == t && [pair action] == a) {
            removablePair = pair;
            break;
        }
    }	
    [pairsForKey removeObject:removablePair];
}
- (NSMutableArray *)pairsForKey:(NSString *)k
{
    return [targetActionPairs objectForKey:k];
}
- (NSArray *)actionsForTarget:(id)t forKey:(NSString *)k
{
    NSMutableArray *list = [NSMutableArray array];
    NSMutableArray *pairsForKey = [self pairsForKey:k];
    for(BNRActionLayerTargetActionPair *pair in pairsForKey) {
        if([pair target] == t)
            [list addObject:NSStringFromSelector([pair action])];
    }
    return [NSArray arrayWithArray:list];
}
- (void)dealloc
{
    [targetActionPairs release];
    [super dealloc];
}
@end

`

How do you use this class? In your view subclass, make sure the type of layer it uses is of type BNRActionLayer (if you are using an explicit layer, you would simply create an instance of BNRActionLayer):
`

@implementation MyView
+ (Class)layerClass
{
    return [BNRActionLayer class];
}
@end

`

When you create an instance of MyView, you can add target-action pairs to it.
`

- (void)applicationDidFinishLaunching:(UIApplication *)app
{
    MyView *v = [[[MyView alloc] initWithFrame:someRect] autorelease];
    [window addSubview:v];
    [(BNRActionLayer *)[v layer] addTarget:self 
                                    action:@selector(viewDidFadeIn:) 
                                    forKey:@"Fade In"];

    [window makeKeyAndVisible];
}

`

Of course, you then need to implement viewDidFadeIn: to do something. Let’s pretend you are fading a view so it can become touchable:
`

- (void)viewDidFadeIn:(BNRActionLayer *)layer
{
    // We can operate on the layer here, do some controllery stuff, and we
    // can also get the this layers owning view. An implicit layer's delegate
    // is always its view (on the iPhone).
    MyView *v = [layer delegate];
    [v setUserInteractionEnabled:YES];
}

`

So, how would you create an animation that will trigger this message to be sent when it finishes? The same way as you would normally create an animation:
`

- (void)activateView:(MyView *)v
{
    CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:@"opacity"];
    [a setToValue:[NSNumber numberWithFloat:1]];
    [a setDuration:1];
    
    // The key here matches the key the target-action pair was added for
    [[v layer] addAnimation:a forKey:@"Fade In"];
}

`

Cool, huh? Disclaimer: I’ve been using this code for a few days and it hasn’t given me any problems. However, that doesn’t mean it is perfect. This was definitely a wake-up-at-3am-from-a-programming-dream-and-write-code type deal. (I dream about programming. I’m weird.) If you have any suggestions for improving the code or find a problem, by all means, please don’t hesitate to comment.

Furthermore, if your understanding of layers is a bit fuzzy (for example, you did a double-take when I said that a layer’s delegate is always its view), be sure to keep an eye out for the Big Nerd Ranch iPhone book, written by Aaron Hillegass and myself, due early next year.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project