UIView animateKeyframesWithDuration vs standard animation block animation differ

iMaddin picture iMaddin · Mar 12, 2014 · Viewed 12.9k times · Source

I recently found some UIView animation code and noticed that it was not using the animateKeyframesWithDuration:delay:options:animations:completion: method correctly and have since been trying to fix it but for some reason the animations are not identical.

Here's the code that I found:

view.transform = CGAffineTransformMakeTranslation(300, 0);

[UIView animateKeyframesWithDuration:duration/4 delay:delay options:0 animations:^{
    // End
    view.transform = CGAffineTransformMakeTranslation(-10, 0);
} completion:^(BOOL finished) {
    [UIView animateKeyframesWithDuration:duration/4 delay:0 options:0 animations:^{
        // End
        view.transform = CGAffineTransformMakeTranslation(5, 0);
    } completion:^(BOOL finished) {
        [UIView animateKeyframesWithDuration:duration/4 delay:0 options:0 animations:^{
            // End
            view.transform = CGAffineTransformMakeTranslation(-2, 0);
        } completion:^(BOOL finished) {
            [UIView animateKeyframesWithDuration:duration/4 delay:0 options:0 animations:^{
                // End
                view.transform = CGAffineTransformMakeTranslation(0, 0);
            } completion:^(BOOL finished) {
            }];
        }];
    }];
}];

There are two problems with this:

  1. One should really use a key frame animation instead of nesting animations inside the completion block.
  2. When using animateKeyframesWithDuration:delay:options:animations:completion: you are supposed to call the addKeyframeWithRelativeStartTime:relativeDuration:animations: method inside the animation block. If you don't call this method then this method will behave like a standard UIView animation block.

See Apple's documentation regarding this method here.

So I tried to fix this by utilising the method correctly:

view.transform = CGAffineTransformMakeTranslation(300, 0);
double frameDuration = 1.0/4.0; // 4 = number of keyframes

[UIView animateKeyframesWithDuration:duration delay:delay options:0 animations:^{
    [UIView addKeyframeWithRelativeStartTime:0*frameDuration relativeDuration:frameDuration animations:^{
        view.transform = CGAffineTransformMakeTranslation(-10, 0);
    }];
    [UIView addKeyframeWithRelativeStartTime:1*frameDuration relativeDuration:frameDuration animations:^{
        view.transform = CGAffineTransformMakeTranslation(5, 0);
    }];
    [UIView addKeyframeWithRelativeStartTime:2*frameDuration relativeDuration:frameDuration animations:^{
        view.transform = CGAffineTransformMakeTranslation(-2, 0);
    }];
    [UIView addKeyframeWithRelativeStartTime:3*frameDuration relativeDuration:frameDuration animations:^{
        view.transform = CGAffineTransformMakeTranslation(0, 0);
    }];
} completion:nil];

I feel like both animations should be identical, however they are not. What did I do wrong in my attempt?

Edit: Example project (Use Command + T to toggle slow animations in the simulator)

Answer

David Rönnqvist picture David Rönnqvist · Mar 12, 2014

tl;dr: They probably look different because they do different things and result in different animations being added to the two views. Unfortunately I don't really know how to fix it.

What's happening behind the first version

As you have already said the first version doesn't use the API correctly. It doesn't add keyframes using addKeyframeWithRelativeStartTime:relativeDuration:animations: but instead changes the properties directly inside the animation block. Then it creates a new "key frame animation" (you will soon see why I used scare quotes there) in the completion block.

Behind the scenes this doesn't actually result in CAKeyframeAnimations (though I don't think you should rely on that fact). This is some logging that I did of the four animation objects that are added to the layer behind view1 in your project. (I cheated a bit and only logged the .m41 part of the transform (which corresponds to translation along x)).

BASIC
keyPath: transform
duration: 0.125
from: 300.0
model value: -10.0
timing: easeInEaseOut

BASIC
keyPath: transform
duration: 0.125
from: -10.0
model value: 5.0
timing: easeInEaseOut

BASIC
keyPath: transform
duration: 0.125
from: 5.0
model value: -2.0
timing: easeInEaseOut

BASIC
keyPath: transform
duration: 0.125
from: -2.0
model value: 0.0
timing: easeInEaseOut

As you can see each animation has a fromValue but neither toValue or byValue. As you can read in the documentation, this means:

fromValue is non-nil. Interpolates between fromValue and the current presentation value of the property.


What's happening behind the second version

On the other hand, the second version that correctly adds keyframes to the animation results in this single CAKeyframeAnimation to be added to the layer behind view2. (I'm still only logging the .m41 part of the transform)

KEYFRAME 
keyPath: transform
duration: 0.500
values: (
    300,
    -10,
    5,
    -2,
    0
)
keyTimes: (
    0.0,
    0.25,
    0.5,
    0.75,
    1.0
)
timing: easeInEaseOut
timings: (nil)
calculationMode: linear

As you can see it animates between the same values with the same times but in a single key frame animation. It also has the same total duration and uses the same timing function.

There is one thing that I'm not understanding. If I modify the real key frame animation (inside of my overridden addAnimation:forKey: before calling super) to explicitly set the the timing function in the array of timing functions, then they do look exactly the same. That is, I do this to the keyframe animation before it is added to the layer.

CAMediaTimingFunction *function = keyFrame.timingFunction;
keyFrame.timingFunction = nil;
keyFrame.timingFunctions = @[function, function, function, function]; 

This trick is super ugly and you should never use it for things other than pure debugging!


So why do they look different?

Well, I guess the answer has to do with the timing functions array of the keyframe animation that UIKit created in the second case. That's pretty much the only conclusion I got.

That said, I don't know if this is a bug or how to fix it. I just know what that code in your project actually does on the Core Animation level.