Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]
Asked Answered
L

15

143

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

Littrell answered 11/2, 2010 at 11:44 Comment(3)
you can do it in the next run loop: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });Muriate
I found many answers below to work for me. Also helpful is Apple's Changing a Layer’s Default Behavior document, which describes the implicit action decision process in detail.Lifton
This is a duplicate question to this one: https://mcmap.net/q/161306/-how-to-disable-calayer-implicit-animationsCowley
P
180

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(),
    ]
Photosphere answered 11/2, 2010 at 13:37 Comment(15)
To prevent movement when changing the frame use the @"position" key.Affectionate
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.Linstock
@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?Tericaterina
@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.Photosphere
is there a place where all these string constants are documented? I can't seem to find it on apple docsSnicker
@Snicker - 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.Photosphere
@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.Snicker
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/Conceptual/…Heman
@BradLarson Not working for me. #21575161Kaiser
@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.Adiaphorous
@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.Adiaphorous
@Adiaphorous 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/Conceptual/…Heman
This doesn't work for me. Can't undo my upvote. See mxcl's answer below. That works.Presbyterian
This was the only perma solution that worked for me in swift. I tried the other solutions on this page in many possible combinations.Waggle
Seems like the most recent link to properties is this: developer.apple.com/library/archive/documentation/Cocoa/…Milklivered
A
90

Also:

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

//foo

[CATransaction commit];
Affectionate answered 30/3, 2011 at 16:11 Comment(5)
You can replace //foo with [self setNeedsDisplayInRect: rect]; [self displayIfNeeded]; to answer the original question.Tehee
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).Steve
Leads to performance issues for me, setting actions is more performantUnbutton
Shorthand: [CATransaction setDisableActions:YES]Hitchcock
Adding to @Hitchcock 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.Terrarium
S
32

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()
Schoolfellow answered 8/7, 2014 at 10:5 Comment(3)
setDisableActions: does the same.Hendecagon
This one was the way simplest solution I got working in Swift!Cornuted
The comment by @Andy is by far the best and easiest way to do this!Atthia
S
23

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.

Silvertongued answered 19/4, 2013 at 2:32 Comment(13)
This isn't working for me. See here.Grannias
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.Silvertongued
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?Silvertongued
I was simply testing with the wrong CALayer instance (I had two at the time).Grannias
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.Serval
@Mecki, this is incorrect. NSNull is allowed as the return value. See actionForKey: method reference (which CALayerDelegate's actionForLayer:forKey: documentation redirects to).Silvertongued
@Serval 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).Silvertongued
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.Silvertongued
Ah, forgot to add: to create NSNull object in Swift, simply use NSNull().Silvertongued
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/…Serval
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?Silvertongued
Thanks for the info, I'll try to use this method in Swift and update the answer accordingly.Silvertongued
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 animationWaggle
D
9

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.

Decury answered 29/10, 2015 at 23:2 Comment(4)
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() ]Athematic
I combined your answer with this one to fix my animating CATextLayer https://mcmap.net/q/161310/-disable-implicit-animation-of-catextlayer-string-propertyCompetition
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!!Anterior
Short and sweet! Great solution!Mortician
L
8

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.

Ludeman answered 21/5, 2013 at 10:48 Comment(1)
This worked for me. Don't forget to subclass CALayer to override the method.Discussant
A
7

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

}
Aenea answered 3/12, 2013 at 21:7 Comment(0)
B
6

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)
Bowling answered 24/3, 2015 at 11:10 Comment(0)
A
6

To disable implicit layer animations in Swift

CATransaction.setDisableActions(true)
Aerostatics answered 10/4, 2018 at 17:8 Comment(1)
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/…Glavin
M
4

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)
    }
}
Messmate answered 11/7, 2019 at 0:54 Comment(0)
C
2

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];
}
Commendation answered 14/1, 2015 at 11:53 Comment(0)
U
2

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
Unstop answered 28/5, 2016 at 9:12 Comment(4)
Please never do this ffsVoidable
@Voidable thanks for that - please explain why this is a bad ideaUnstop
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).Voidable
It's really is a shame that view.layer?.actions = [:] doesn't really work. Setting the speed is ugly but works.Sapele
M
0

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

[UIView performWithoutAnimation:^{
    // apply changes
}];
Mensural answered 1/8, 2015 at 18:39 Comment(3)
I do not believe that this method blocks CALayer animations.Adiaphorous
@Adiaphorous Ah I think you're right. Didn't know as much in August. Should I delete this answer?Mensural
:-) I'm never sure either, sorry! The comments communicate the uncertainty anyway, so it's probably okay.Adiaphorous
M
0

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.

Merozoite answered 11/3, 2016 at 20:28 Comment(0)
M
0

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

Monomial answered 6/4, 2016 at 17:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.