Custom property animation with actionForKey: how do I get a new value for a property?

In the CALayer subclass I'm working on, I have a custom property that I want to automatically activate, i.e. if the property is called "myProperty", I need the following code:

[myLayer setMyProperty:newValue]; 

To make a smooth animation from the current value to "newValue".

Using the override approach actionForKey: and needsDisplayForKey: (see the following code) I was able to make it work very nicely, just to interpolate between the old and the new value.

My problem is that I want to use a slightly different animation duration or path (or something else) depending on the current value and the new property value, and I could not figure out how to get the new value from inside ActionForKey:

Thanks in advance

 @interface ERAnimatablePropertyLayer : CALayer { float myProperty; } @property (nonatomic, assign) float myProperty; @end @implementation ERAnimatablePropertyLayer @dynamic myProperty; - (void)drawInContext:(CGContextRef)ctx { ... some custom drawing code based on "myProperty" } - (id <CAAction>)actionForKey:(NSString *)key { if ([key isEqualToString:@"myProperty"]) { CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key]; theAnimation.fromValue = [[self presentationLayer] valueForKey:key]; ... I want to do something special here, depending on both from and to values... return theAnimation; } return [super actionForKey:key]; } + (BOOL)needsDisplayForKey:(NSString *)key { if ([key isEqualToString:@"myProperty"]) return YES; return [super needsDisplayForKey:key]; } @end 
+7
source share
3 answers

You need to avoid custom getters and setters for the properties you want to animate.

Override the didChangeValueForKey: method. Use it to set the model value for the property you want to animate.

Do not set toValue in action animations.

 @interface MyLayer: CALayer @property ( nonatomic ) NSUInteger state; @end 

-

 @implementation MyLayer @dynamic state; - (id<CAAction>)actionForKey: (NSString *)key { if( [key isEqualToString: @"state"] ) { CABasicAnimation * bgAnimation = [CABasicAnimation animationWithKeyPath: @"backgroundColor"]; bgAnimation.fromValue = [self.presentationLayer backgroundColor]; bgAnimation.duration = 0.4; return bgAnimation; } return [super actionForKey: key]; } - (void)didChangeValueForKey: (NSString *)key { if( [key isEqualToString: @"state"] ) { const NSUInteger state = [self valueForKey: key]; UIColor * newBackgroundColor; switch (state) { case 0: newBackgroundColor = [UIColor redColor]; break; case 1: newBackgroundColor = [UIColor blueColor]; break; case 2: newBackgroundColor = [UIColor greenColor]; break; default: newBackgroundColor = [UIColor purpleColor]; } self.backgroundColor = newBackgroundColor.CGColor; } [super didChangeValueForKey: key]; } @end 
+2
source

Core Animation calls actionForKey: before updating the property value. It starts the action after updating the value of the property by sending it runActionForKey:object:arguments: The CAAnimation implementation runActionForKey:object:arguments: simply calls [object addAnimation:self forKey:key] .

Instead of returning the animation from actionForKey: you can return a CAAction that, when launched, creates and sets the animation. Something like that:

 @interface MyAction: NSObject <CAAction> @property (nonatomic, strong) id priorValue; @end @implementation MyAction - (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict { ERAnimatablePropertyLayer *layer = anObject; CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key]; id newValue = [layer valueForKey:key]; // Set up animation using self.priorValue and newValue to determine // fromValue and toValue. You could use a CAKeyframeAnimation instead of // a CABasicAnimation. [layer addAnimation:animation forKey:key]; } @end @implementation ERAnimatablePropertyLayer - (id <CAAction>)actionForKey:(NSString *)key { if ([key isEqualToString:@"myProperty"]) { MyAction *action = [[MyAction alloc] init]; action.priorValue = [self valueForKey:key]; return action; } return [super actionForKey:key]; } 

In this answer you can find a working example of a similar method (in Swift, cornerRadius animation) .

+2
source

You can save old and new values ​​in CATransaction.

 -(void)setMyProperty:(float)value { NSNumber *fromValue = [NSNumber numberWithFloat:myProperty]; [CATransaction setValue:fromValue forKey:@"myPropertyFromValue"]; myProperty = value; NSNumber *toValue = [NSNumber numberWithFloat:myProperty]; [CATransaction setValue:toValue forKey:@"myPropertyToValue"]; } - (id <CAAction>)actionForKey:(NSString *)key { if ([key isEqualToString:@"myProperty"]) { CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key]; theAnimation.fromValue = [[self presentationLayer] valueForKey:key]; theAnimation.toValue = [CATransaction objectForKey:@"myPropertyToValue"]; // here you do something special. } return [super actionForKey:key]; } 
+1
source

All Articles