Animating a custom property of CALayer subclass - iphone

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?

Yes, it's possible (only in the latest Core Animation releases though, I believe, i.e. iPhone 3.0+ and OS X 10.6+).
Make your property dynamic so that CA implements the accessors for you:
#dynamic myInt;
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];
}
}
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.
If you also want to enable implicit animations for this property, also override actionForKey:.

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.

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

Related

C4: Add panning to an object other than "self"

I watched the C4 tutorial on adding a pan gesture to an object and animating it to return to its original position when the panning is finished. I'm trying to add this to three individual objects. I have it working with one object so far to move it and reset it to a CGPoint, but for it to work, I have to add the pan gesture to "self", not the object. For reference, I'm pretty much using the code from here:
http://www.c4ios.com/tutorials/interactionPanning
If I add the gesture to the object itself, sure, it pans around, but then it just leaves itself at the last touch location. However, I'm assuming that leaving the gesture on "self" will affect more than just the object I want to move, and I want to be able to move the three objects individually.
I'm using roughly the same modification to the "move" method that's used in the example:
-(void)move:(UIPanGestureRecognizer *)recognizer {
[character move:recognizer];
if (recognizer.state == UIGestureRecognizerStateEnded) {
[character setCenter: charStartOrigin];
}
}
And then a new method to spawn the object:
-(void)createCharacters {
character = [C4Shape ellipse:charStart];
[character addGesture:PAN name:#"pan" action:#"move:"];
[self.canvas addShape:character];
}
The example link you are working from is sneaky. Since I knew that there was only going to be one object on the canvas I knew I could make it look like I was panning the label. This won't work for multiple objects, as you have already figured out.
To get different objects to move independently, and recognize when they are done being dragged, you need to subclass the objects and give them their own "abilities".
To do this I:
Subclass C4Shape
Add custom behaviour to the new class
Create subclassed objects on the canvas
The code for each step looks like the following:
subclassing
You have to create a subclass that gives itself some behaviour. Since you're working with shapes I have done it this way as well. I call my subclass Character, its files look like this:
Character.h
#import "C4Shape.h"
#interface Character : C4Shape
#property (readwrite, atomic) CGPoint startOrigin;
#end
I have added a property to the shape so that I can set its start origin (i.e. the point to which it will return).
Character.m
#import "Character.h"
#implementation Character
-(void)setup {
[self addGesture:PAN name:#"pan" action:#"move:"];
}
-(void)move:(UIGestureRecognizer *)sender {
if(sender.state == UIGestureRecognizerStateEnded) {
self.center = self.startOrigin;
} else {
[super move:sender];
}
}
#end
In a subclass of a C4 object, setup gets called in the same way as it does for the canvas... So, this is where I add the gesture for this object. Setup gets run after new or alloc/init are called.
The move: method is where I want to override with custom behaviour. In this method I catch the gesture recognizer, and if it's state is UIGestureRecognizerStateEnded then I want to animate back to the start origin. Otherwise, I want it to move: like it should so I simply call [super move:sender] which runs the default move: method.
That's it for the subclass.
Creating Subclassed Objects
My workspace then looks like the following:
#import "C4WorkSpace.h"
//1
#import "Character.h"
#implementation C4WorkSpace {
//2
Character *charA, *charB, *charC;
}
-(void)setup {
//3
CGRect frame = CGRectMake(0, 0, 100, 100);
//4
frame.origin = CGPointMake(self.canvas.width / 4 - 50, self.canvas.center.y - 50);
charA = [self createCharacter:frame];
frame.origin.x += self.canvas.width / 4.0f;
charB = [self createCharacter:frame];
frame.origin.x += self.canvas.width / 4.0f;
charC = [self createCharacter:frame];
//5
[self.canvas addObjects:#[charA,charB,charC]];
}
-(Character *)createCharacter:(CGRect)frame {
Character *c = [Character new];
[c ellipse:frame];
c.startOrigin = c.center;
c.animationDuration = 0.25f;
return c;
}
#end
I have added a method to my workspace that creates a Character object and adds it to the screen. This method creates a Character object by calling its new method (I have to do it this way because it is a subclass of C4Shape), turns it into an ellipse with the frame I gave it, sets its startOrigin, changes its animationDuration.
What's going on with the rest of the workspace is this (NOTE: the steps are marked in the code above):
I #import the subclass so that I can create objects with it
I create 3 references to Character objects.
I create a frame that I will use to build each of the new objects
For each object, I reposition frameby changing its origin and then use it to create a new object with the createCharacter: method I wrote.
I add all of my new objects to the canvas.
NOTE: Because I created my subclass with a startOrigin property, I am able within that class to always animate back to that point. I am also able to set that point from the canvas whenever I want.

Properties on CALayer subclass aren't getting observed by CATransaction

I have a subclass of CALayer with a custom property, declared as such:
#interface MyLayer : CALayer
#property (nonatomic,retain) NSNumber *customValue;
#end
#implementation MyLayer
#synthesize customValue = _customValue;
#end
I want to animate this property inside of an explicit CATranasction, so i set up a delegate with the actionForLayer:forKey: method implemented which returns an animation, however any changes to someMyLayerInstance.customValue inside of [CATransaction begin] ... [CATransaction end] do not result in actionForLayer:forKey getting called with a corresponding 'key' value.
However, nuking the property on MyLayer and making changes to myLayerInstance by calling setValue:forKey: does result in actionForLayer:forKey: getting called.
It appears that this is because CALayer does some mojo for key/value coding for undefined properties. How can I recreate this mojo so that I can declare properties on MyLayer, but still have them be observed by the animation delegate?
The most important thing is that you need to implement all CALayer accessors using #dynamic. Do not use #synthesize and do not implement the accessors directly. CALayer generates all its own property handlers (as you've indirectly discovered), and you need to let those be used.
You also need to let CALayer know that this property is display-impacting (which you may have already done given your other comments). If you haven't, you do this by implementing +needsDisplayForKey: and returning YES for your key.
Here's an example of a CALayer that animates a custom property (taken from Chapter 7 of iOS 5 Programming Pushing the Limits. The full sample code is available at the Wiley site.) This example implements actionForKey: in the layer, but you can still implement that part in the delegate if you prefer.
#implementation CircleLayer
#dynamic radius;
...
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:#"radius"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
- (id < CAAction >)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString:#"radius"]) {
CABasicAnimation *anim = [CABasicAnimation
animationWithKeyPath:#"radius"];
anim.fromValue = [[self presentationLayer]
valueForKey:#"radius"];
return anim;
}
}
return [super actionForKey:key];
}
#end

What is the best method to reuse animations for cocos2d?

I would like to be able to allocate a few actions so that I can use and reuse them again and again on whatever target CCNode I want. I assumed I could just load them up with a generic NSObject class in an init then call them when nessicary from a method but was unable to.
Was I just inept at my attempt or is there a more elegant solution?
#implementation Animation
#synthesize animation;
-(id) init
{
if ( (self=[super init]) ) {
animation = [CCScaleTo actionWithDuration: 0.15 scale: 2.0];
}
return self;
}
- (id) myAnimation
{
return animation;
}
[targetCCNode runAction:[Animation myAnimation]];
Creation an action is very fast. I don't think it's reasonable. If you want just to create actions with some common configuration then I suggest you to create (or use an existing one) a class with static methods for creating this actions. Or a singleton with non-static methods if you require some context when creating actions

Dealing with objects from different classes

I have 3 classes of objects. All 3 classes share some properties in common, as color, text, etc.
For example, I can have this
Class1 *objectA = [[Class1 alloc] init];
objectA.myColor = [UIColor redColor];
Class2 *objectB = [[Class2 alloc] init];
objectA.myColor = [UIColor redColor];
Class3 *objectC = [[Class3 alloc] init];
objectA.myColor = [UIColor redColor];
... etc.
Now I need, for example, to create a method that can change the color of a given object, whatever class it represents.
A typical method would be
- (void) changeColor:(Class1*) myOBJ toColor:(UIColor*)myColor {
myOBJ.color = myColor;
}
when in fact I need this
- (void) changeColor:(???) myOBJ toColor:(UIColor*)myColor {
myOBJ.color = myColor;
}
// what to put on ??? to make it generic? Is this a "whatever" kind?
thanks
EDIT
the problem of using this approach
- (void) changeColor:(id)myOBJ toColor:(UIColor*)myColor {
if ([myOBJ respondsToSelector:#selector(setColor:)]) {
myOBJ.color = myColor;
}
}
is this. Imagine I want to set the frame of the object.
Then I will have to have this:
- (void) changeColor:(id)myOBJ newFrame:(CGRect)myFrame {
if ([umID isKindOfClass:[Class1 class]]) {
Class1 *oneObj = (Class1 *)myObj;
oneObj.frame = myFrame;
}
if ([umID isKindOfClass:[Class2 class]])
Class2 *oneObj = (Class2 *)myObj;
oneObj.frame = myFrame;
}
if ([umID isKindOfClass:[Class3 class]])
Class3 *oneObj = (Class3 *)myObj;
oneObj.frame = myFrame;
}
}
in other words, I will have to repeat the same stuff 3 times... right?
in other words, the problem is not solved as this is the same of having 3 methods, one for each class.
Maybe you can use protocols? Make Class1, Class2 and Class3 conform to a protocol with a property myColor. Then you could have a method like this (assuming your classes are of type UIView and your protocol is called ColorProtocol):
- (void) changeColor:(UIView<ColorProtocol>*) myOBJ toColor:(UIColor*)myColor {
myOBJ.color = myColor;
myOBJ.frame = ...;
}
Here is what your protocol definition could look like:
#protocol ColorProtocol
#property (nonatomic, retain) UIColor *myColor;
#end
Change your class definition files (.h) as follows to specify that you will conform to the protocol:
interface Class1 : UIView <ColorProtocol> {...}
In the implementation files (.m) you must simply synthesize the myColor property to conform to the ColorProtocol:
#synthesize myColor;
If your classes are very similar, using inheritance might be even simpler though. Check out Philip Regan's answer.
You have a couple options. The simplest, and "most dangerous" approach is to use a type id. This will let you pass in any object, but you'll want to test that it actually has a color property before you try and set it.
- (void) changeColor:(id)myOBJ toColor:(UIColor*)myColor {
if ([myOBJ respondsToSelector:#selector(setColor:)]) {
myOBJ.color = myColor;
}
}
(That said, with the responds to selector check, this approach isn't all that dangerous, and it's much more flexible than the next idea.)
Another approach is to have all your objects inherit from a shared base class that has a color property. Then your parameter type would be the base class. This approach could be considered "safer" as the compiler would check that you're passing in the correct type of object. This approach also requires considerably more code.
If you want to use the first approach, but set something other than color, adjust the respondsToSelector: call appropriately.
- (void) changeFrame:(id)myOBJ newFrame:(CGRect)myFrame {
if ([myOBJ respondsToSelector:#selector(setFrame:)]) {
myOBJ.frame = myFrame;
}
}
In general, if you want to know if an object supports propertyX, use [myOBJ respondsToSelector:#selector(setPropertyX:)]. If the passed in object is declared as id, you can then call [myOBJ setPropertyX:newPropertyValue] or myObj.propertyX = newPropertyValue.
If you have multiple classes that share characteristics, then, if at all possible, I suggest refactoring the class structure so that those characteristics are contained in an umbrella parent class, we'll call it ClassZ. ClassZ's subclasses can override things as needed. Otherwise, let the method in the parent class handle it for you. Then, your method turns back into this...
- (void) changeColor:(ClassZ *) myOBJ toColor:(UIColor*)myColor {
myOBJ.color = myColor; // note, myObj is ClassZ, not the subclasses.
}
Otherwise, you are stuck with id and testing the individual classes.
use [object setFrame:newFrame]; instead of object.frame = newFrame;
and instead of oldFrame = object.frame; use oldFrame = [object frame];
??? will be 'id'.

Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]

I've got a layer with some complex drawing code in its -drawInContext: method. I'm trying to minimize the amount of drawing I need to do, so I'm using -setNeedsDisplayInRect: to update just the changed parts. This is working splendidly. However, when the graphics system updates my layer, it's transitioning from the old to the new image using a cross-fade. I'd like it to switch over instantly.
I've tried using CATransaction to turn off actions and set the duration to zero, and neither work. Here's the code I'm using:
[CATransaction begin];
[CATransaction setDisableActions: YES];
[self setNeedsDisplayInRect: rect];
[CATransaction commit];
Is there a different method on CATransaction I should use instead (I also tried -setValue:forKey: with kCATransactionDisableActions, same result).
You can do this by setting the actions dictionary on the layer to return [NSNull null] as an animation for the appropriate key. For example, I use
NSDictionary *newActions = #{
#"onOrderIn": [NSNull null],
#"onOrderOut": [NSNull null],
#"sublayers": [NSNull null],
#"contents": [NSNull null],
#"bounds": [NSNull null]
};
layer.actions = newActions;
to disable fade in / out animations on insertion or change of sublayers within one of my layers, as well as changes in the size and contents of the layer. I believe the contents key is the one you're looking for in order to prevent the crossfade on updated drawing.
Swift version:
let newActions = [
"onOrderIn": NSNull(),
"onOrderOut": NSNull(),
"sublayers": NSNull(),
"contents": NSNull(),
"bounds": NSNull(),
]
Also:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
//foo
[CATransaction commit];
When you change the property of a layer, CA usually creates an implicit transaction object to animate the change. If you do not want to animate the change, you can disable implicit animations by creating an explicit transaction and setting its kCATransactionDisableActions property to true.
Objective-C
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
// change properties here without animation
[CATransaction commit];
Swift
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
In addition to Brad Larson's answer: for custom layers (that are created by you) you can use delegation instead of modifying layer's actions dictionary. This approach is more dynamic and may be more performant. And it allows disabling all implicit animations without having to list all animatable keys.
Unfortunately, it's impossible to use UIViews as custom layer delegates, because each UIView is already a delegate of its own layer. But you can use a simple helper class like this:
#interface MyLayerDelegate : NSObject
#property (nonatomic, assign) BOOL disableImplicitAnimations;
#end
#implementation MyLayerDelegate
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
if (self.disableImplicitAnimations)
return (id)[NSNull null]; // disable all implicit animations
else return nil; // allow implicit animations
// you can also test specific key names; for example, to disable bounds animation:
// if ([event isEqualToString:#"bounds"]) return (id)[NSNull null];
}
#end
Usage (inside the view):
MyLayerDelegate *delegate = [[MyLayerDelegate alloc] init];
// assign to a strong property, because CALayer's "delegate" property is weak
self.myLayerDelegate = delegate;
self.myLayer = [CALayer layer];
self.myLayer.delegate = delegate;
// ...
self.myLayerDelegate.disableImplicitAnimations = YES;
self.myLayer.position = (CGPoint){.x = 10, .y = 42}; // will not animate
// ...
self.myLayerDelegate.disableImplicitAnimations = NO;
self.myLayer.position = (CGPoint){.x = 0, .y = 0}; // will animate
Sometimes it's convenient to have view's controller as a delegate for view's custom sublayers; in this case there is no need for a helper class, you can implement actionForLayer:forKey: method right inside the controller.
Important note: don't try to modify the delegate of UIView's underlying layer (e.g. to enable implicit animations) — bad things will happen :)
Note: if you want to animate (not disable animation for) layer redraws, it is useless to put [CALayer setNeedsDisplayInRect:] call inside a CATransaction, because actual redrawing may (and probably will) happen sometimes later. The good approach is to use custom properties, as described in this answer.
Here's a more efficient solution, similar to accepted answer but for Swift. For some cases it will be better than creating a transaction every time you modify the value which is a performance concern as others have mentioned e.g. common use-case of dragging the layer position around at 60fps.
// Disable implicit position animation.
layer.actions = ["position": NSNull()]
See apple's docs for how layer actions are resolved. Implementing the delegate would skip one more level in the cascade but in my case that was too messy due to the caveat about the delegate needing to be set to the associated UIView.
Edit: Updated thanks to the commenter pointing out that NSNull conforms to CAAction.
Actually, I didn't find any of the answers to be the right one. The method that solves the problem for me was this:
- (id<CAAction>)actionForKey:(NSString *)event {
return nil;
}
Then you can whatever logic in it, to disable a specific animation, but since I wanted to removed them all, I returned nil.
Based on Sam's answer, and Simon's difficulties... add the delegate reference after creating the CSShapeLayer:
CAShapeLayer *myLayer = [CAShapeLayer layer];
myLayer.delegate = self; // <- set delegate here, it's magic.
... elsewhere in the "m" file...
Essentially the same as Sam's without the ability to toggle via the custom "disableImplicitAnimations" variable arrangement. More of a "hard-wire" approach.
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
// disable all implicit animations
return (id)[NSNull null];
// allow implicit animations
// return nil;
// you can also test specific key names; for example, to disable bounds animation:
// if ([event isEqualToString:#"bounds"]) return (id)[NSNull null];
}
To disable implicit layer animations in Swift
CATransaction.setDisableActions(true)
Found out a simpler method to disable action inside a CATransaction that internally calls setValue:forKey: for the kCATransactionDisableActions key:
[CATransaction setDisableActions:YES];
Swift:
CATransaction.setDisableActions(true)
Updated for swift and disabling only one implicit property animation in iOS not MacOS
// Disable the implicit animation for changes to position
override open class func defaultAction(forKey event: String) -> CAAction? {
if event == #keyPath(position) {
return NSNull()
}
return super.defaultAction(forKey: event)
}
Another example, in this case eliminating two implicit animations.
class RepairedGradientLayer: CAGradientLayer {
// Totally ELIMINATE idiotic implicit animations, in this example when
// we hide or move the gradient layer
override open class func defaultAction(forKey event: String) -> CAAction? {
if event == #keyPath(position) {
return NSNull()
}
if event == #keyPath(isHidden) {
return NSNull()
}
return super.defaultAction(forKey: event)
}
}
Add this to your custom class where you are implementing -drawRect() method. Make changes to code to suite your needs, for me 'opacity' did the trick to stop cross-fade animation.
-(id<CAAction>) actionForLayer:(CALayer *)layer forKey:(NSString *)key
{
NSLog(#"key: %#", key);
if([key isEqualToString:#"opacity"])
{
return (id<CAAction>)[NSNull null];
}
return [super actionForLayer:layer forKey:key];
}
If you ever need a very quick (but admittedly hacky) fix it might be worth just doing (Swift):
let layer = CALayer()
// set other properties
// ...
layer.speed = 999
As of iOS 7 there's a convenience method that does just this:
[UIView performWithoutAnimation:^{
// apply changes
}];
To disable the annoying (blurry) animation when changing the string property of a CATextLayer, you can do this:
class CANullAction: CAAction {
private static let CA_ANIMATION_CONTENTS = "contents"
#objc
func runActionForKey(event: String, object anObject: AnyObject, arguments dict: [NSObject : AnyObject]?) {
// Do nothing.
}
}
and then use it like so (don't forget to set up your CATextLayer properly, e.g. the correct font, etc.):
caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]
You can see my complete setup of CATextLayer here:
private let systemFont16 = UIFont.systemFontOfSize(16.0)
caTextLayer = CATextLayer()
caTextLayer.foregroundColor = UIColor.blackColor().CGColor
caTextLayer.font = CGFontCreateWithFontName(systemFont16.fontName)
caTextLayer.fontSize = systemFont16.pointSize
caTextLayer.alignmentMode = kCAAlignmentCenter
caTextLayer.drawsAsynchronously = false
caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]
caTextLayer.contentsScale = UIScreen.mainScreen().scale
caTextLayer.frame = CGRectMake(playbackTimeImage.layer.bounds.origin.x, ((playbackTimeImage.layer.bounds.height - playbackTimeLayer.fontSize) / 2), playbackTimeImage.layer.bounds.width, playbackTimeLayer.fontSize * 1.2)
uiImageTarget.layer.addSublayer(caTextLayer)
caTextLayer.string = "The text you want to display"
Now you can update caTextLayer.string as much as you want =)
Inspired by this, and this answer.
Try this.
let layer = CALayer()
layer.delegate = hoo // Same lifecycle UIView instance.
Warning
If you set delegate of UITableView instance, sometimes happen crash.(Probably scrollview's hittest called recursively.)