Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]

IphoneIosCore AnimationCalayer

Iphone Problem Overview


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

Iphone Solutions


Solution 1 - Iphone

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(),
    ]

Solution 2 - Iphone

Also:

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

//foo

[CATransaction commit];

Solution 3 - Iphone

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

Solution 4 - Iphone

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.

Solution 5 - Iphone

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.

Solution 6 - Iphone

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.

Solution 7 - Iphone

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];

}

Solution 8 - Iphone

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)

Solution 9 - Iphone

To disable implicit layer animations in Swift

CATransaction.setDisableActions(true)

Solution 10 - Iphone

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

Solution 11 - Iphone

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];
}

Solution 12 - Iphone

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

Solution 13 - Iphone

As of iOS 7 there's a convenience method that does just this:

[UIView performWithoutAnimation:^{
    // apply changes
}];

Solution 14 - Iphone

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.

Solution 15 - Iphone

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBen GottliebView Question on Stackoverflow
Solution 1 - IphoneBrad LarsonView Answer on Stackoverflow
Solution 2 - IphonemxclView Answer on Stackoverflow
Solution 3 - Iphoneuser3378170View Answer on Stackoverflow
Solution 4 - IphoneskozinView Answer on Stackoverflow
Solution 5 - IphoneJarrod SmithView Answer on Stackoverflow
Solution 6 - IphoneSimonView Answer on Stackoverflow
Solution 7 - IphonebobView Answer on Stackoverflow
Solution 8 - IphonerounakView Answer on Stackoverflow
Solution 9 - IphonepawpoiseView Answer on Stackoverflow
Solution 10 - IphoneGayleDDSView Answer on Stackoverflow
Solution 11 - IphoneKamran KhanView Answer on Stackoverflow
Solution 12 - IphoneMartin CRView Answer on Stackoverflow
Solution 13 - IphoneWarplingView Answer on Stackoverflow
Solution 14 - IphoneErik ZivkovicView Answer on Stackoverflow
Solution 15 - Iphonegm333View Answer on Stackoverflow