30

I have a CALayer subclass, MyLayer, that has a NSInteger property called myInt. I'd really like to animate this property via CABasicAnimation, but it seems CABasicAnimation only works on so-called "animatable" properties (bounds, position, etc). Is there something I can override to make my custom myInt property animatable?

jemmons
  • 18,605
  • 8
  • 55
  • 84

3 Answers3

70

Yes, it's possible (only in the latest Core Animation releases though, I believe, i.e. iPhone 3.0+ and OS X 10.6+).

  1. Make your property dynamic so that CA implements the accessors for you:

    @dynamic myInt;
    
  2. Tell the layer that changes of the property require redrawing:

    + (BOOL)needsDisplayForKey:(NSString*)key {
        if ([key isEqualToString:@"myInt"]) {
            return YES;
        } else {
            return [super needsDisplayForKey:key];
        }
    }
    
  3. Use the value of myInt in your drawInContext: method. Now, when you animate myInt, Core Animation will interpolate the values for each step of the animation and repeatedly ask the layer to draw itself.

  4. If you also want to enable implicit animations for this property, also override actionForKey:.

gerry3
  • 21,420
  • 9
  • 66
  • 74
Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • Thank you! This has me so close to a solution I can taste it! -drawInContext: is called several times with myInt interpolated correctly. Unfortunately CGContextDrawImage() no longer functions while the animation is running. Rects I fill or paths I stroke are all drawn correctly, but the image normally drawn by CGContextDrawImage() just disappears until the animation is over as if CGContextDrawImage() is never called. I'm baffled. Any thoughts? – jemmons Mar 07 '10 at 23:52
  • No idea, sorry. I hope you find a solution. – Ole Begemann Mar 08 '10 at 00:09
  • 3
    That's ok. You're reply is still one of the best written answers I've seen on Stack Overflow :) – jemmons Mar 08 '10 at 00:37
  • 9
    Dear future googlers: CGContextDrawImage() wasn't drawing because I was storing its CGImageRef in an ivar. When CoreAnimation spawns off its multiple presentation layers (or whatever) it doesn't copy ivars, only KVC values. Storing my image in a UIImage and then [self setValue:myUIImage forKey:@"image"] for later retrieval in -drawInContext: did the trick. – jemmons Mar 08 '10 at 01:45
  • 1
    @dynamic myInt didn't work for me; I had to @synthesize myInt to get this to work. – titaniumdecoy Nov 09 '10 at 19:35
  • I got a problem that the layer did call drawInContext, but the value of the property is not changed according to my toValue and fromValue. – Slavik Jan 27 '12 at 16:10
  • Is it possible to animate custom properties that are NOT on CALayer subclasses? For example, I have a custom property on a UIView subclass I want to animate. Possible or "custom CALayer subclass properties only"? – zakdances Apr 25 '13 at 04:15
  • 1
    @jemmons the right solution in that case would be to copy those properties in initWithLayer: – NachoSoto Sep 03 '13 at 01:17
  • Save me so much time... Thanks for you explanation. – Danyun Liu Apr 02 '14 at 02:42
  • @OleBegemann here's a challenge ... http://stackoverflow.com/q/25016137/294884 (and man, you need the 50 pts right! :) ) – Fattie Sep 26 '14 at 16:03
6

There is a way to retain the iVars of your custom CALayer subclasses. You override initWithLayer:, the method which is called to create a copy of custom layers. For example, if you have a layer in which you want to create a custom property called 'angle', you might use the following code:

@implementation AngledLayer
@synthesize angle = _angle

// Tell Core Animation that this key should be animated
+ (BOOL) needsDisplayForKey:(NSString *)key
{
    if ([key isEqualToString:@"angle"]) return YES;
    return [super needsDisplayForKey:key];
}


// Make sure that, when the layer is copied, so is the custom ivar
- (id) initWithLayer:(id)layer
{
    self = [super initWithLayer:layer];
    if (self) {
        AngledLayer *angledVersion = (AngledLayer *)layer;
        self.angle = angledVersion.angle;
    }
    return self;
}

And bob's your uncle! Note that you can't use this object with implicit animation, for which you'd also have to overide the actionForKey: method.

Ash
  • 61
  • 1
  • 1
  • 1
    This doesn't work for me. Inside `initWithLayer`, the layer always has the starting value of the property, even while animating. The setter is called somewhere else. – zakdances Apr 26 '13 at 11:56
  • 1
    I'm seeing the same as yourfriendzak. While the combination of initWithLayer and needsDisplayForkey allows the animation to happen, as soon as it ends, the layer goes back to its starting value. I'm still searching for a solution. – Danny Sung Jul 29 '13 at 17:18
  • Okay, it looks like I didn't realize that @dynamic causes the setter to automatically animate. But if for some reason you needed to do more to it, you probably need to create your own setter that has it's own animation block so that you can change your layer property before the animation takes place. See [link](http://stackoverflow.com/questions/11515647/objective-c-cabasicanimation-applying-changes-after-animation) – Danny Sung Jul 29 '13 at 17:31
0

Swift version

// Custom property
@NSManaged public var progress: CGFloat
    
open override class func needsDisplay(forKey key: String) -> Bool {
    if key == "progress" {
        return true
    }
    return super.needsDisplay(forKey: key)
}
Evgeny Karkan
  • 8,782
  • 2
  • 32
  • 38