Search code examples
ioscore-animation

iOS animateWithDuration complete immediately for hidden/show or even opacity


I want to animate my CALayer to show up for a while, then fade out, here is my code

[_msgLayer removeAllAnimations];
[UIView animateWithDuration:10 delay:0 options:0 animations:^ {
     _msgLayer.hidden = false;
    NSLog(@"showing");
} completion:^(BOOL finished) {
    NSLog(@"showing done, finished=%d", finished);
    [UIView animateWithDuration:10 delay:40 options:0 animations:^ {
        _msgLayer.hidden = true;
    } completion:^(BOOL hideFinished) {
        NSLog(@"hiding done, finished=%d", hideFinished);
    }];
}];

However, the animation simply doesn't work as I expected, everything complete almost immediately

2014-10-26 10:11:28.249 Foobar[91761:6827358] showing
2014-10-26 10:11:28.254 Foobar[91761:6827358] showing done, finished=1
2014-10-26 10:11:28.255 Foobar[91761:6827358] hiding done, finished=1

I see some similar questions out there, some people say hidden is not animatable, but the document says it's animatable. And I also tried opacity, then same result, the animation still finish immediately. Why is that? and how can I solve that?

The _msgLayer is my own class inherits CALayer and it has own drawing method. And this animation is called from networking event, like server send a message to iPhone, then I show the message on the screen.


Solution

  • The problem is that UIView animations are meant for animating views.

    A high overview of what's happening when you use UIView animations is that the block gets executed, which changes the properties of the view which in turn changes the backing layer (or changes the layer properties directly). When the (backing) layer's properties changes it asks its delegate (which always is the view if the layer is backing a view) to provide an action for that animation. The view can then check if the change happened inside of an animation block or not. If the change happened inside of an animation block, the view will return an animation with the correct duration, timing curve, etc.

    However, for view's that aren't attached to a layer, there is no delegate to ask. Instead the layer continues to look for an "action" (a more general term for an animation) and eventually ends up picking the default action for that layer class. This behavior is outlined in the documentation for the -actionForKey: method on CALayer.

    The default action is almost always (animations of paths being one exception) a 0.25 second long basic animation. This is the animation you are seeing. Layers actually animate their changes by default (it's a view behavior to disable this) so you would see this animation both from changes inside the animation block and form outside the animation block.


    If you want to read more about this:

    The first third of my objc.io article here explain the interaction between the view and the layer more closely. I also have a blog post here that among other things explain the difference between implicit and explicit animations.


    The above explains why you are seeing this behavior. To "fix" it you would either go to a higher abstraction and use a view instead of a layer or you would create the animation objects yourself and add them to the layer (the work that UIKit is doing at the view level).