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

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
Ben Gottlieb
  • 85,404
  • 22
  • 176
  • 172
  • 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(), ^{ });` – Hashem Aboonajmi Jun 17 '15 at 14:04
  • 1
    I found many answers below to work for me. Also helpful is Apple's [Changing a Layer’s Default Behavior](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/ReactingtoLayerChanges/ReactingtoLayerChanges.html) document, which describes the implicit action decision process in detail. – ɲeuroburɳ Oct 26 '15 at 16:41
  • This is a duplicate question to this one: https://stackoverflow.com/a/54656717/5067402 – Ryan Francesconi Feb 12 '19 at 21:43

15 Answers15

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(),
    ]
Nikolay Suvandzhiev
  • 8,465
  • 6
  • 41
  • 47
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • 26
    To prevent movement when changing the frame use the `@"position"` key. – mxcl Mar 30 '11 at 16:11
  • 12
    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. – Andrew Dec 29 '11 at 22:58
  • @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? – Ser Pounce Jan 11 '12 at 21:10
  • @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. – Brad Larson Jan 11 '12 at 21:33
  • is there a place where all these string constants are documented? I can't seem to find it on apple docs – pqnet Apr 03 '12 at 15:37
  • @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. – Brad Larson Apr 03 '12 at 16:26
  • 1
    @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`. – pqnet Apr 08 '12 at 17:41
  • 14
    There are actually constants for these 'special' strings not representing a property (e.g. kCAOnOrderOut for @"onOrderOut") well-documented here: https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/Actions.html#//apple_ref/doc/uid/TP40006095-SW1 – Patrick Pijnappel Aug 05 '12 at 21:28
  • @BradLarson Not working for me. http://stackoverflow.com/questions/21574661/uitableviewcell-label-displayed-with-little-slide-up-animation – Geek Feb 07 '14 at 17:12
  • @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 Dec 03 '14 at 10:17
  • @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. – Benjohn Dec 03 '14 at 10:24
  • 2
    @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: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/ReactingtoLayerChanges/ReactingtoLayerChanges.html#//apple_ref/doc/uid/TP40004514-CH7-SW1 – Patrick Pijnappel Dec 04 '14 at 07:53
  • 1
    This doesn't work for me. Can't undo my upvote. See mxcl's answer below. That works. – Sam Soffes Mar 23 '15 at 21:50
  • 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 Jul 30 '17 at 11:46
  • 1
    Seems like the most recent link to properties is this: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html – Fyodor Volchyok Jan 13 '22 at 15:52
90

Also:

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

//foo

[CATransaction commit];
mxcl
  • 26,392
  • 12
  • 99
  • 98
  • 3
    You can replace `//foo` with `[self setNeedsDisplayInRect: rect]; [self displayIfNeeded];` to answer the original question. – Karoy Lorentey Jun 23 '11 at 00:56
  • 1
    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). – Joe D'Andrea Sep 12 '11 at 13:51
  • 3
    Leads to performance issues for me, setting actions is more performant – Pascalius Mar 08 '12 at 10:44
  • 26
    Shorthand: `[CATransaction setDisableActions:YES]` – titaniumdecoy Oct 02 '12 at 00:38
  • 7
    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. – Hlung Nov 19 '14 at 09:45
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()
user3378170
  • 2,371
  • 22
  • 11
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.

Community
  • 1
  • 1
skozin
  • 3,789
  • 2
  • 21
  • 24
  • This isn't working for me. [See here.](http://stackoverflow.com/questions/25830050/disabling-calayer-implicit-animations) – aleclarson Sep 14 '14 at 04:30
  • 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 Nov 14 '14 at 01:28
  • 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? – skozin Nov 14 '14 at 01:31
  • I was simply testing with the wrong `CALayer` instance (I had two at the time). – aleclarson Nov 14 '14 at 01:39
  • 1
    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. – Mecki May 27 '16 at 15:09
  • @Mecki, this is incorrect. NSNull is allowed as the return value. See [actionForKey:](https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CALayer_class/index.html#//apple_ref/occ/instm/CALayer/actionForKey:) method reference (which `CALayerDelegate`'s `actionForLayer:forKey:` documentation redirects to). – skozin May 28 '16 at 11:56
  • @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](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNull_Class/index.html#//apple_ref/occ/cl/NSNull) (currently, `CAAction` is the first protocol in the list). – skozin May 28 '16 at 11:58
  • 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 May 28 '16 at 12:05
  • Ah, forgot to add: to create `NSNull` object in Swift, simply use `NSNull()`. – skozin May 28 '16 at 12:17
  • 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 https://s33.postimg.org/si135uetr/Screen_Shot_2016_05_29_at_12_17_20.png (this is where **your link** takes me) and here https://s33.postimg.org/j5a5ciegv/Screen_Shot_2016_05_29_at_12_18_10.png – Mecki May 29 '16 at 10:25
  • Hmm, that's very strange. That's what I see: [NSNull](https://s33.postimg.org/sh3pu7bxb/2016_05_29_13_38_59.png) and [actionForKey:](https://s33.postimg.org/z5ck1llkf/2016_05_29_13_38_43.png). 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](https://s33.postimg.org/m3odiw8xr/2016_05_29_13_44_55.png)? – skozin May 29 '16 at 10:46
  • Thanks for the info, I'll try to use this method in Swift and update the answer accordingly. – skozin May 29 '16 at 10:47
  • 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` – Sentry.co Jul 30 '17 at 11:48
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.

Sam Soffes
  • 14,831
  • 9
  • 76
  • 80
sleep
  • 4,855
  • 5
  • 34
  • 51
  • I combined your answer with this one to fix my animating CATextLayer http://stackoverflow.com/a/5144221/816017 – Erik Živković Mar 11 '16 at 20:18
  • 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() ] – user5649358 Dec 07 '15 at 09:45
  • 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!! – PlateReverb Apr 02 '17 at 17:12
  • Short and sweet! Great solution! – David H Nov 15 '19 at 22:53
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.

MJN
  • 10,748
  • 1
  • 23
  • 32
Simon
  • 1,076
  • 7
  • 13
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];

}
bob
  • 7,539
  • 2
  • 46
  • 42
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)
rounak
  • 9,217
  • 3
  • 42
  • 59
6

To disable implicit layer animations in Swift

CATransaction.setDisableActions(true)
Chris Long
  • 3,007
  • 4
  • 30
  • 37
pawpoise
  • 61
  • 1
  • 2
  • 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: https://developer.apple.com/documentation/quartzcore/catransaction/1448276-disableactions – Austin Dec 03 '18 at 23:15
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)
    }
}
Fattie
  • 27,874
  • 70
  • 431
  • 719
GayleDDS
  • 4,443
  • 22
  • 23
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];
}
Kamran Khan
  • 1,367
  • 1
  • 15
  • 19
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
Martin CR
  • 1,250
  • 13
  • 25
  • 4
    Please never do this ffs – m1h4 Sep 09 '17 at 09:19
  • @m1h4 thanks for that - please explain why this is a bad idea – Martin CR Sep 10 '17 at 10:56
  • 4
    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). – m1h4 Sep 11 '17 at 11:45
  • 1
    It's really is a shame that `view.layer?.actions = [:]` doesn't really work. Setting the speed is ugly but works. – tcurdt Aug 29 '19 at 00:32
0

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

[UIView performWithoutAnimation:^{
    // apply changes
}];
Warpling
  • 2,024
  • 2
  • 22
  • 38
  • 2
    I do not believe that this method blocks [CALayer animations](https://www.objc.io/issues/5-ios7/iOS7-hidden-gems-and-workarounds/#blocking-animations). – Benjohn Feb 19 '16 at 18:46
  • 1
    @Benjohn Ah I think you're right. Didn't know as much in August. Should I delete this answer? – Warpling Feb 20 '16 at 19:22
  • :-) I'm never sure either, sorry! The comments communicate the uncertainty anyway, so it's probably okay. – Benjohn Feb 22 '16 at 11:02
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.

Community
  • 1
  • 1
Erik Živković
  • 4,867
  • 2
  • 35
  • 53
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.)

gm333
  • 136
  • 1
  • 10