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:
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)
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.
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 betweenfromValue
and the current presentation value of the property.
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!
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.