4
votes

In Core Animation Programming guide, there is one paragraph about How to Animate Layer-Backed Views, it says:

If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.

There are also an example:

[UIView animateWithDuration:1.0 animations:^{
   // Change the opacity implicitly.
   myView.layer.opacity = 0.0;

   // Change the position explicitly.
   CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:@"position"];
   theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
   theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
   theAnim.duration = 3.0;
   [myView.layer addAnimation:theAnim forKey:@"AnimateFrame"];
}];

In my opinion, it tells that if I don't issue Core Animation calls from inside a view-based animation block, there will no animation.

But it seems that if I add the core animation calls directly without view-based animation block, it works the same.

Have I missed something ?

3
The distinction between layer-backed views and layer-hosted views and all of the quirks that come with it, only applies to the OS X and has to do with maintaining compatibility with pre-10.5 API. Even the first versions of iOS (called iPhone OS (those were the days)) had full support for Core Animation layers for every single view.David Rönnqvist
@DavidRönnqvist It seems not a hint to the question.KudoCC
(Maybe I could have phrased that better). The simplified version of that quote from the docs is: "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.". The OP is doing an explicit animation inside the block, which isn't necessary. ... maybe I should just take a minute and answer the question instead of posting vague comments 😐David Rönnqvist
@DavidRönnqvist The result shows it disables the implicit animation, but in the docs it didn't mention it.KudoCC
"The UIView class disables layer animations by default but reenables them inside animation blocks"David Rönnqvist

3 Answers

18
votes

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.

0
votes

I will explain it with a simple demonstration (example).
Add below code in your view controller.(don't forget to import QuartzCore)

@implementation ViewController{
    UIView *view;
    CALayer *layer;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [self.view addSubview:view];
    view.backgroundColor =[UIColor greenColor];
    layer = [CALayer layer];
    layer.frame =CGRectMake(0, 0, 50, 50);
    layer.backgroundColor =[UIColor redColor].CGColor;
    [view.layer addSublayer:layer];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    layer.frame =CGRectMake(70, 70, 50, 50);
}

See that in toucesBegan method there is no UIVIew animate block.
When you run the application and click on the screen, the opacity of the layer animates.This is because by default these are animatable.By default all the properties of a layer are animatable.

Consider now the case of layer backed views.
Change the code to below

- (void)viewDidLoad
{
    [super viewDidLoad];
    view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [self.view addSubview:view];
    view.backgroundColor =[UIColor greenColor];
//    layer = [CALayer layer];
//    layer.frame =CGRectMake(0, 0, 50, 50);
//    layer.backgroundColor =[UIColor redColor].CGColor;
//    [view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    view.layer.opacity =0.0;

//    layer.frame =CGRectMake(70, 70, 50, 50);
}

You might think that this will also animate its view.But it wont happen.Because layer backed views by default are not animatable.
To make those animations happen, you have to explicitly embed the code in UIView animate block.As shown below,

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [UIView animateWithDuration:2.0 animations:^{
        view.layer.opacity =0.0;
    }];

//    layer.frame =CGRectMake(70, 70, 50, 50);
}

That explains that,

The UIView class (which obviously was layer backed) disables layer animations by default but reenables them inside animation blocks.So any changes you make outside of an animation block are not animated.

0
votes

Well, I've never used explicit Core Animation inside a view animation block. The documentation it seems to be not clear at all.
Probably the meaning is something like that, it's just a guess:

If you want to animate properties of the view that are linked to the backed layer you should wrap them into a view animation block. In the view's layer if you try to change the layer opacity this is not animated, but if wrap into a view animation block it is.

In your snippet you are directly creating a basic animation thus explicitly creating an animation. Probably the doc just want to point out the differences between views and layers. In the latter animations on most properties are implicit.
You can see the difference if you write something like that:

[UIView animateWithDuration:1.0 animations:^{
   // Change the opacity implicitly.
   myView.layer.opacity = 0.0;
}];

This will be animated.

   myView.layer.opacity = 0.0;

This will not be animated.

// Change the position explicitly.
   CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:@"position"];
   theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
   theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
   theAnim.duration = 3.0;
   [myView.layer addAnimation:theAnim forKey:@"AnimateFrame"];

This will be animated.