Animating CALayer content

Out of the box, CALayer supports many animatable properties where a change will automatically create a CAAnimation for that property. But, if you look at the list of properties, it is clear that they are all of the form “stuff that OpenGL does while compositing” and have nothing to do with the inner texture representing your content. You are entirely responsible for telling CoreAnimation when your content has changed.

Recently I wanted to write a layer that had its content animate, but as far as I could determine CALayer doesn't support this out of the box, though it is really pretty close. If you add an @dynamic property to a CALayer subclass and set it, your layer will be sent -actionForKey: and the returned animation will be passed to your -addAnimation:forKey:.

The problem is that CALayer doesn't know that this change means it needs to update your contents property too, so the animation will happen but no display change will happen.

The API on CALayer really isn't sufficient for this task, so it isn't too surprising it isn't supported. Since CALayer is a generic KVC container you can squirrel away random keys/value pairs in it and use them for whatever purposes you want. But it has no way of knowing that a particular property change should provoke an update to the content, and updating the content unnecessarily would seriously hurt performance.

I've whipped up some sample code with a superclass that shows one approach to handling this.

The sample subclass looks like:

@interface WaveLayer : ContentAnimatingLayer

@property CGFloat phase, frequency, amplitude;

@end

@implementation WaveLayer

@dynamic phase, frequency, amplitude;

+ (NSSet *)keyPathsForValuesAffectingContent;

{

static NSSet *keys = nil;

if (!keys)

keys = [[NSSet alloc] initWithObjects:@"phase",

@"frequency", @"amplitude", nil];

return keys;

}

- (void)drawInContext:(CGContextRef)ctx;

{

… draw a sine wave …

}

@end

 

The ContentAnimatingLayer superclass provides some bookkeeping and basic support for updating the content when the content-affecting properties are changed. First, it adds support for -actionFor; like many other key-based Cocoa protocols (Radar #6372335). On top of this, it adds support for determining which properties are content-affecting with +keyPathsForValuesAffectingContent and -isContentAnimation:. Active content-affecting animations are then tracked and an update timer is fired when there is at least one such animation.

This makes it as easy as I'd hoped it would be to write content animating layers; hopefully you'll find this useful too! I've asked Apple to add this to CALayer in the future. If you'd like it too, you can reference my Radar #6372372.

The sample code is just that, my first cut at a sample. Some possible issues/improvements:

  • The -actionFor support may or may not be fast enough (or really not even necessary).
  • Instead of following the class-based KVO customization pattern, CALayers are often customized by the delegate. So, the NSSet-returning method should maybe be an instance method or maybe the -isContentAnimation: should be split up and call a new -layer:isContentAnimatingKey: delegate method.
  • It would be nicer to tie into the normal CoreAnimation timing mechanism instead of creating a per-instance NSTimer. Even without that, it might be a bit more efficient to have a single animation timer for all instances of ContentAnimatingLayer. Also, right now the timer doesn't get registered for the event tracking runloop mode, though it maybe should.
  • This approach assumes that a change to a content-affecting key requires the entire content be redrawn. This may or may not be the case for any particular layer. A change to a content-animating property might want to specify an NSRect-based animation that describes the dirty area.