Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]

51,401

Solution 1

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

Also:

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

//foo

[CATransaction commit];

Solution 3

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

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

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.

Share:
51,401
Ben Gottlieb
Author by

Ben Gottlieb

Newton, Palm, Mac, now iPhone. Love this industry!

Updated on September 30, 2020

Comments

  • Ben Gottlieb
    Ben Gottlieb over 3 years

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

  • mxcl
    mxcl about 13 years
    To prevent movement when changing the frame use the @"position" key.
  • Karoy Lorentey
    Karoy Lorentey almost 13 years
    You can replace //foo with [self setNeedsDisplayInRect: rect]; [self displayIfNeeded]; to answer the original question.
  • Joe D'Andrea
    Joe D'Andrea over 12 years
    Thanks! This lets me set an animated flag on my custom view as well. Handy for use within a table view cell (where cell reuse can lead to some trippy animations while scrolling).
  • Andrew
    Andrew over 12 years
    Also be sure to add the @"hidden" property in the action dictionary too if you are toggling the visibility of a layer that way and wish to disable the opacity animation.
  • Ser Pounce
    Ser Pounce over 12 years
    @brad larson - do you know how you'd use this to disable the navigation animation in the navigation bar (ie when a view controller gets pushed on the stack), ie what key would you use?
  • Brad Larson
    Brad Larson over 12 years
    @CoDEFRo - That's totally unrelated to these actions. This is just for Core Animation's implicit animations on layers. What you describe is something internal to UIKit, so it's not controllable via what I show here.
  • Pascalius
    Pascalius about 12 years
    Leads to performance issues for me, setting actions is more performant
  • pqnet
    pqnet about 12 years
    is there a place where all these string constants are documented? I can't seem to find it on apple docs
  • Brad Larson
    Brad Larson about 12 years
    @pqnet - Some are simply animatable properties. The more subtle ones can be discovered by overriding -animationForKey: and seeing which keys are animated in response to an action.
  • pqnet
    pqnet about 12 years
    @BradLarson that's the same idea i came up with after some struggling (i overrode actionForKey: instead), discovering fontSize, contents, onLayout and bounds. It seems like you can specify any key you could use in setValue:forKey: method, actually specifying complex key paths like bounds.size.
  • Patrick Pijnappel
    Patrick Pijnappel over 11 years
    There are actually constants for these 'special' strings not representing a property (e.g. kCAOnOrderOut for @"onOrderOut") well-documented here: developer.apple.com/library/mac/#documentation/Cocoa/Concept‌​ual/…
  • titaniumdecoy
    titaniumdecoy over 11 years
    Shorthand: [CATransaction setDisableActions:YES]
  • Geek
    Geek about 10 years
    @BradLarson Not working for me. stackoverflow.com/questions/21574661/…
  • pronebird
    pronebird over 9 years
    setDisableActions: does the same.
  • aleclarson
    aleclarson over 9 years
    This isn't working for me. See here.
  • skozin
    skozin over 9 years
    Hmmm. I have never had any issues with this approach. The code in the linked question looks ok and probably the issue is caused by some other code.
  • skozin
    skozin over 9 years
    Ah, I see that you have already sorted out that it was wrong CALayer that prevented noImplicitAnimations from working. Maybe you should mark your own answer as correct and explain what was wrong with that layer?
  • aleclarson
    aleclarson over 9 years
    I was simply testing with the wrong CALayer instance (I had two at the time).
  • Hlung
    Hlung over 9 years
    Adding to @titaniumdecoy comment, just in case anyone got confused (like me), [CATransaction setDisableActions:YES] is a shorthand for just the [CATransaction setValue:forKey:] line. You still need the begin and commit lines.
  • Benjohn
    Benjohn over 9 years
    @Patrick !That's amazingly useful, many thanks! My solution had been NSStringFromSelector(@selector(contents)), which at least has some compile time checking. Yours is much better. Could the Brad update the answer to include this? I'm happy to edit, but large improvements like this are often rejected in moderation.
  • Benjohn
    Benjohn over 9 years
    @patrick on looking at this, unfortunately only three of the properties seem to have keys defined for them: kCAOnOrderIn, kCAOnOrderOut & kCATransition. So, Brad can probably leave the answer as it is.
  • Patrick Pijnappel
    Patrick Pijnappel over 9 years
    @Benjohn Only the keys that don't have a corresponding property have constants defined. BTW, the link seems to be dead, here's the new URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptu‌​al/…
  • Sam Soffes
    Sam Soffes about 9 years
    This doesn't work for me. Can't undo my upvote. See mxcl's answer below. That works.
  • user5649358
    user5649358 over 8 years
    No need to create a NullAction for Swift, NSNull conforms to CAAction already so you can do the same you do in objective C: layer.actions = [ "position" : NSNull() ]
  • Jambaman
    Jambaman over 8 years
    This one was the way simplest solution I got working in Swift!
  • Benjohn
    Benjohn about 8 years
    I do not believe that this method blocks CALayer animations.
  • Warpling
    Warpling about 8 years
    @Benjohn Ah I think you're right. Didn't know as much in August. Should I delete this answer?
  • Benjohn
    Benjohn about 8 years
    :-) I'm never sure either, sorry! The comments communicate the uncertainty anyway, so it's probably okay.
  • Erik Zivkovic
    Erik Zivkovic about 8 years
    I combined your answer with this one to fix my animating CATextLayer stackoverflow.com/a/5144221/816017
  • Mecki
    Mecki almost 8 years
    Nice solution... but NSNull does not implement the CAAction protocol and this is no protocol that only has optional methods. This code as well crash and you can't even translate that to swift. Better solution: Make your object conform to the CAAction protocol (with an empty runActionForKey:object:arguments: method that does nothing) and return self instead of [NSNull null]. Same effect but safe (will not crash for sure) and also works in Swift.
  • skozin
    skozin almost 8 years
    @Mecki, this is incorrect. NSNull is allowed as the return value. See actionForKey: method reference (which CALayerDelegate's actionForLayer:forKey: documentation redirects to).
  • skozin
    skozin almost 8 years
    @Mecki Specifically, it says: "The delegate must do one of the following: 1) Return the action object for the given key. 2) Return the NSNull object if it does not handle the action." Also, NSNull does conform to CAAction, see the list of protocols it conforms to in its reference (currently, CAAction is the first protocol in the list).
  • skozin
    skozin almost 8 years
    Your suggestion of implementing CAAction by some object you have control of will work, though, but I don't think it's necessary, even in Swift. I can assure you that I had no single crash caused by returning NSNull from this method. Also, I doubt Apple documentation and examples would suggest something that could crash your app.
  • skozin
    skozin almost 8 years
    Ah, forgot to add: to create NSNull object in Swift, simply use NSNull().
  • Mecki
    Mecki almost 8 years
    Sorry, I have no idea what page you are seeing but your link takes me to a page where NSNull definitely does NOT conform to the CAAction protocol. When I try to return NSNull() in Swift, I get a compile time error telling me exactly that. And when I read the delegate documentation, it nowhere says that you may return NSNull. See also here s33.postimg.org/si135uetr/… (this is where your link takes me) and here s33.postimg.org/j5a5ciegv/…
  • skozin
    skozin almost 8 years
    Hmm, that's very strange. That's what I see: NSNull and actionForKey:. And what are you seeing when you follow the link to CA programming guide in the answer ("you can use delegation")? Do you see this?
  • skozin
    skozin almost 8 years
    Thanks for the info, I'll try to use this method in Swift and update the answer accordingly.
  • PlateReverb
    PlateReverb about 7 years
    This was a great fix for my problem of needed to bypass the "animation" delay when changing the color of CALayer lines in my project. Thanks!!
  • Sentry.co
    Sentry.co almost 7 years
    This was the only perma solution that worked for me in swift. I tried the other solutions on this page in many possible combinations.
  • Sentry.co
    Sentry.co almost 7 years
    NO luck for me in swift with this answer. Tried @Mecki's suggestion as well. What worked in the end was setting layer?.actions = ["sublayers":NSNull(),"content":NSNull(),"onOrderOut":NSNull‌​(),"bounds":NSNull()‌​,"hidden":NSNull(),"‌​position":NSNull()]/‌​/avoids implicit animation
  • m1h4
    m1h4 over 6 years
    Please never do this ffs
  • Martin CR
    Martin CR over 6 years
    @m1h4 thanks for that - please explain why this is a bad idea
  • m1h4
    m1h4 over 6 years
    Because if one needs to turn off implicit animations there is a mechanism for doing that (either a ca transaction with temporarily disabled actions or explicitly setting empty actions onto a layer). Just setting the animation speed to something hopefully high enough to make it seem instant causes loads of unnecessary performance overhead (which the original author mentions is relevant for him) and potential for various race-conditions (the drawing is still done into a seperate buffer to be animated into the display at a later point - to be precise, for your case above, at 0.25/999 sec later).
  • Aᴄʜᴇʀᴏɴғᴀɪʟ
    Aᴄʜᴇʀᴏɴғᴀɪʟ almost 6 years
    The comment by @Andy is by far the best and easiest way to do this!
  • Austin
    Austin over 5 years
    Thanks for this answer. I first tried using disableActions() as it sounds like it does the same thing, but it's actually to get the current value. I think it's marked @discardable too, making this harder to spot. Source: developer.apple.com/documentation/quartzcore/catransaction/…
  • tcurdt
    tcurdt over 4 years
    It's really is a shame that view.layer?.actions = [:] doesn't really work. Setting the speed is ugly but works.
  • David H
    David H over 4 years
    Short and sweet! Great solution!
  • eonil
    eonil over 3 years
    This worked for me. Don't forget to subclass CALayer to override the method.
  • Fyodor Volchyok
    Fyodor Volchyok over 2 years
    Seems like the most recent link to properties is this: developer.apple.com/library/archive/documentation/Cocoa/…