When Exactly Do Implicit Animations Take Place in iOS

When exactly do implicit animations take place in iOS?

UIKit disables implicit animations. To be more specific, a CALayer associated with a UIView will never implicitly animate. CALayers that you create yourself and that are not associated with a UIView will buy into the normal implicit animation machinery.

If you're interested in how this works, implicit animations happen after the normal -actionForKey: lookup. If there's a CAAction for a given property, it's used. Otherwise, the implicit animation is used. In the case of a CALayer associated with a UIView, UIView implements the -actionForLayer:forKey: delegate method, and when not in a UIView animation block it always returns [NSNull null] to signify that the action lookup should stop here. This prevents implicit animations from working. Inside of a UIView animation block, it constructs its own action to represent the current UIView animation settings. Again, this prevents implicit animations.

If you need to animate CALayer properties that UIView won't do for you, you can use an explicit CAAnimation subclass (such as CABasicAnimation).

How can I enable implicit UIView animation?

You can use animateWithDuration:

[UIView animateWithDuration:2.0 animations:^{
viewA.frame = newFrame;
}];

CALayer: Where does the implicit animation comes from?

The CALayer's SDK default animations are implemented in the .m, which you cannot get it in normal way.

And the SDK offers optional ways for user to use customised animation:

delegate:

You can use a delegate object to provide the layer’s contents, handle the layout of any sublayers, and provide custom actions in response to layer-related changes. The object you assign to this property should implement one or more of the methods of the CALayerDelegate informal protocol. ...

actions:

The default value of this property is nil. You can use this dictionary to store custom actions for your layer. The contents of this dictionary searched as part of the standard implementation of the actionForKey: method.

they're just for animation customisation.

e.g. you can create a custom CALayer subclass (say CustomLayer) which override the -actionForKey:

- (id<CAAction>)actionForKey:(NSString *)event
{
if ([event isEqualToString:@"strokeStart"] || [event isEqualToString:@"strokeEnd"]) {
CABasicAnimation * strokAnimation = [CABasicAnimation animationWithKeyPath:event];
strokAnimation.removedOnCompletion = NO;
strokAnimation.fillMode = kCAFillModeForwards;
strokAnimation.duration = .3f;
strokAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokAnimation.fromValue = [self.presentationLayer valueForKey:event];
return strokAnimation;
}
return [super actionForKey:event];
}

in this way, whenever you update strokStart & strokend for your CustomLayer instance, it'll invoke the strokAnimation you offered instead of the default animation that SDK implemented.


But style, it's for setting the animatable properties' value w/o animation (cause by default, whenever you update CALayer instance's animatable properties' value, it'll invoke default animation if you don't offer custom one):

An optional dictionary used to store property values that aren't explicitly defined by the layer.

If the style dictionary does not define a value for an attribute, the receiver’s defaultValueForKey: method is called. The default value of this property is nil.

e.g. like strokeStart & strokeEnd, their default values are 0.f & 1.f, you can modify style to use customized default value, like:

aLayer.style = @{@"strokeStart" : @.2f,
@"strokeEnd" : @.6f};

in this way, the layer's animation won't be invoked, like in the initialisation step, especially when you've offered a complex animation for the key, it'll helps a lot.


For +defaultActionForKey:, it's more like user defined default actions, as the doc said:

Returns a suitable action object for the given key or nil of no action object was associated with that key.

Classes that want to provide default actions can override this method and use it to return those actions.

You can override this method like -actionForKey:, below is the order to check whether the action exists when you modified the related property:

  1. Check whether the delegate exists & delegate method -actionForLayer:forKey: is implemented;
  2. Check whether the action of key was offered in -actionForKey:;
  3. Check whether the action of key exists in actions while invoking [super actionForKey:event] after step 2;
  4. Follows step 3 if no action of key exists in actions, check whether the action of key exists in +defaultActionForKey:.
  5. Use SDK implemented default actions, which we cannot get (or might have unknown event name), as far as I know.

Generally, +defaultActionForKey: is like a global customised setting in class level for your CALayer subclass, while actions, -actionForLayer:forKey: & -actionForKey: to handle special layers.

Use core animation on an Layer-Backed view

tl;dr: The documentation only refers to implicit animations. Explicit animations work fine outside of animation blocks.


My paraphrasing of the documentation

The simplified version of that quote from the docs is something like (me paraphrasing it):

UIView have disabled implicit animations except for within animation blocks. If you want to do implicit layer animations you must do them inside an animation block.

What is implicit animations and how do they work?

Implicit animations is what happens when an animatable property of a standalone layer changes. For example, if you create a layer and change it's position it's going to animate to the new position. Many, many layer properties have this behaviour by default.

It happens something like this:

  1. a transaction is started by the system (without us doing anything)
  2. the value of a property is changed
  3. the layer looks for the action for that property
  4. at some point the transaction is committed (without us doing anything)
  5. the action that was found is applied

Notice that there is no mention of animation above, instead there is the word "action". An action in this context refers to an object which implements the CAAction protocol. It's most likely going to be some CAAnimation subclass (like CABasicAnimation, CAKeyframeAnimation or CATransition) but is built to work with anything that conforms to that protocol.

How does it know what "action" to take?

Finding the action for that property happens by calling actionForKey: on the layer. The default implementation of this looks for an action in this order:

This search happens in this order (ref: actionForKey: documentation)

  1. If the layer has a delegate and that delegate implements the Accessing the Layer’s Filters method, the layer calls that method. The delegate must do one of the following:

    • Return the action object for the given key.
    • Return nil if it does not handle the action.
    • Return the NSNull object if it does not handle the action and the search should be terminated.
  2. The layer looks in the layer’s actions dictionary.
  3. The layer looks in the style dictionary for an actions dictionary that contains the key.
  4. The layer calls its defaultActionForKey: method to look for any class-defined actions.
  5. The layer looks for any implicit actions defined by Core Animation.

What is UIView doing?

In the case of layers that are backing views, the view can enable or disable the actions by implementing the delegate method actionForLayer:forKey. For normal cases (outside an animation block) the view disables the implicit animations by returning [NSNull null] which means:

it does not handle the action and the search should be terminated.

However, inside the animation block, the view returns a real action. This can easily be verified by manually invoking actionForLayer:forKey: inside and outside the animation block. It could also have returned nil which would cause the layer to keep looking for an action, eventually ending up with the implicit actions (if any) if it wouldn't find anything before that.

When an action is found and the transaction is committed the action is added to the layer using the regular addAnimation:forKey: mechanism. This can easily be verified by creating a custom layer subclass and logging inside -actionForKey: and -addAnimation:forKey: and then a custom view subclass where you override +layerClass and return the custom layer class. You will see that the stand alone layer instance logs both methods for a regular property change but the backing layer does not add the animation, except when within a animation block.

Why this long explanation of implicit animations?

Now, why did I give this very long explanation of how implicit animations work? Well, it's to show that they use the same methods that you use yourself with explicit animations. Knowing how they work, we can understand what it means when the documentation say: "The UIView class disables layer animations by default but reenables them inside animation blocks".

The reason why explicit animations aren't disabled by what UIView does, is that you are doing all the work yourself: changing the property value, then calling addAnimation:forKey:.

The results in code:

Outside of animation block:

myView.backgroundColor       = [UIColor redColor];           // will not animate :(
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)

myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // will not animate :(
[myView.layer addAnimation:myAnimation forKey:@"myKey"]; // animates :)

Inside of animation block:

myView.backgroundColor       = [UIColor redColor];           // animates :)
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)

myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
[myView.layer addAnimation:myAnimation forKey:@"myKey"]; // animates :)

You can see above that explicit animations and implicit animations on standalone layers animate both outside and inside of animation blocks but implicit animations of layer-backed views does only animate inside the animation block.

CABasicAnimation does not animate correctly when I update model layer

I think that it's easiest to explain what is happening for each of the three locations and then a "conclusion" at the end.

I'm also adding some illustrations, showing exactly the behaviour that you are mentioning in your question so that it will be easier to follow for someone who hasn't tried these three things themselves. I'm also extending the illustration to show both a stand alone layer and a backing layer (one that is attached to a view) and I will explain the difference where there is one.

Location 1

In the first location, the model value is updated before the animation is created. Once this is done, the transform property holds the updatedTransform. This means that when you read the transform from the layer for the fromValue, you get the updatedValue back. This in turn means that both to and from value are the same so you can't see the animation.

Sample Image

One thing that could have made this location work as expected is to read the oldValue before assigning the new value and then use that as the fromValue. This will look as expected.

// Location 1
CATransform3D oldValue = layer.transform; // read the old value first
layer.transform = updatedTransform; // then update to the new value

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
anim.duration = 1.0;
anim.fromValue = [NSValue valueWithCATransform3D:oldValue];
anim.toValue = [NSValue valueWithCATransform3D:updatedTransform];

Location 2

In the second example, the value isn't yet updated when you read the transform for the from value, so the fromValue and toValue are different. After that, the model value is updated to it's final value. There is actually a difference between the stand-alone layer and the backing layer here, but we don't see it. The transform property on CALayer is animatable and will automatically perform an "implicit" animation when the value changes. This means that an animation will be added to the layer for the "transform" key path. The view, however, disables this behaviour when the change happens outside of an animation block, so there is not implicit animation there.

The reason why we don't see the implicit animation is that the "explicit" animation is added afterwards for the same key path. This means that the only explicit animation will be visible, in both cases, even thought there are two animation running on the stand-alone layer (more on that later). If you are feeling cautious, then you could disable the implicit action for the stand-alone layer (more on that later).

Sample Image

Location 3

This leaves us with the last location. In this case the animation is created just as above, with different fromValue and toValue. The only difference is the order of adding the explicit animation and changing the property which triggers an implicit animation. In this case the implicit animation is added after the explicit animation and they both run(!). Both animations actually ran for location 2, but we couldn't see it because the explicit (longer) animation was added before.

Sample Image

Since everything is moving so fast, I slowed down the entire layer to try and illustrate what is happening when two animations are running at once. This way it becomes much easier to see what happens when the implicit animation ends. I've overlaid the well behaving backing layer and the misbehaving stand-alone layer and made them both 50% transparent. The dashed outline is the original frame.

Sample Image

A short description of what is happening: the blue view get's only the explicit animation added to it (which has a 1 second duration). The orange layer first has the same explicit animation added to it and then has an implicit 0.25 second animation added to it. Neither explicit nor the implicit animations are "additive", meaning their toValue and fromValue are used as-is.

Disclaimer: I do not work at Apple and I haven't seen the source code for Core Animation so what I'm about to say is guesswork based on how things behave.

In my understanding (see disclaimer) this is what happens for every screen refresh to produce the animation: for the current time stamp, the layer goes through the animations in the order they were added and updates the presentation values. In this case, the explicit animation sets a rotation transform, then the implicit animation comes and sets another rotation transform that completely overrides the explicit transform.

If an animation is configured to be "additive", it will add to the presentation values instead of overwriting (which is super powerful). Even with additive animations, order still matters. A non-additive animation could come later and overwrite the whole thing.

Since the implicit animation is shorter than the explicit one, we see that for the first part of the total animation, the values are strictly coming from the implicit animation (which was added last). Once the implicit animation finishes, the only remaining animation is the explicit animation which has been running underneath the implicit one, all this time. So when the implicit animation finishes, the explicit animation has already progressed 0.25 seconds and we see that the orange layer jumps back to the same value as the blue view, instead of jumping back to the beginning.

Where should we update the value?

At this point, the question is, how can we prevent two animations from being added and where should we update the value? The location where the value is updated doesn't prevent there from being two animations (but it can affect how the end result looks).

To prevent two actions from being added to the stand-alone layer, we temporarily disable all "actions" (a more general term for an animation):

[CATransaction begin];
[CATransaction setDisableActions:YES]; // actions are disabled for now
layer.transform = updatedTransform;
[CATransaction commit]; // until here

When we do this, only one animation is added to the layer so either location 2 or 3 works. That is simply a matter of taste. If you read the oldValue, the you can also use location 1 (as long as the action is disabled).

If you are animating a backing layer then you don't have to disable actions (the view does that for you) but it also doesn't hurt to do so.


At this point I could keep going about other ways to configure an animation, what an additive animation is, and why you needed to specify both the toValue and fromValue in this case. But I think that I have answered the question you asked and that this answer already is a bit on the long side.

Why changing values of a CAGradientLayer causes an automatic animation? And how can I avoid it?

Many of the animatable properties have default animations. When you set them, the default animation happens.

You can override the default animation by setting the property in an animation block. Any time you change a property in an animation block, any previous animations for that property are replaced by your new animation.



Related Topics



Leave a reply



Submit