13

I am using a CABasicAnimation to rotate a UIImageView 90 degrees clockwise, but I need to have it rotate a further 90 degrees later on from its position after the initial 90 degree rotation.

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.duration = 10;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(0)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(90)];
[_myview.layer addAnimation:animation forKey:@"90rotation"];

Using the code above works initially, the image stays at a 90 degree angle. If I call this again to make it rotate a further 90 degrees the animation starts by jumping back to 0 and rotating 90 degrees, not 90 to 180 degrees.

I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.

Any ideas?

BytesGuy
  • 4,097
  • 6
  • 36
  • 55

2 Answers2

67

tl;dr: It is very easy to misuse removeOnCompletion = NO and most people don't realize the consequences of doing so. The proper solution is to change the model value "for real".


First of all: I'm not trying to judge or be mean to you. I see the same misunderstanding over and over and I can see why it happens. By explaining why things happen I hope that everyone who experience the same issues and sees this answer learn more about what their code is doing.

What went wrong

I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.

That is very true and it's exactly what happens. Computers are funny in that sense. They always to exactly what you tell them and not what you want them to do.

removeOnCompletion = NO can be a bitch

In your case the villain is this line of code:

animation.removedOnCompletion = NO;

It is often misused to keep the final value of the animation after the animation completes. The only problem is that it happens by not removing the animation from the view. Animations in Core Animation doesn't alter the underlying property that they are animating, they just animate it on screen. If you look at the actual value during the animation you will never see it change. Instead the animation works on what is called the presentation layer.

Normally when the animation completes it is removed from the layer and the presentation layer goes away and the model layer appears on screen again. However, when you keep the animation attached to the layer everything looks as it should on screen but you have introduced a difference between what the property says is the transform and how the layer appears to be rotated on screen.

When you configure the animation to be additive that means that the from and to values are added to the existing value, just as you said. The problem is that the value of that property is 0. You never change it, you just animate it. The next time you try and add that animation to the same layer the value still won't be changed but the animation is doing exactly what it was configured to do: "animate additively from the current value of the model".

The solution

Skip that line of code. The result is however that the rotation doesn't stick. The better way to make it stick is to change the model. Set the new end value of the rotation before animating the rotation so that the model looks as it should when the animation gets removed.

byValue is like magic

There is a very handy property (that I'm going to use) on CABasicAnimation that is called byValue that can be used to make relative animations. It can be combined with either toValue and fromValue to do many different kinds of animations. The different combinations are all specified in its documentation (under the section). The combination I'm going to use is:

byValue and toValue are non-nil. Interpolates between (toValue - byValue) and toValue.

Some actual code

With an explicit toValue of 0 the animation happens from "currentValue-byValue" to "current value". By changing the model first current value is the end value.

NSString *zRotationKeyPath = @"transform.rotation.z"; // The killer of typos

// Change the model to the new "end value" (key path can work like this but properties don't)
CGFloat currentAngle = [[_myview.layer valueForKeyPath:zRotationKeyPath] floatValue];
CGFloat angleToAdd   = M_PI_2; // 90 deg = pi/2
[_myview.layer setValue:@(currentAngle+angleToAdd) forKeyPath:zRotationKeyPath]; 

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:zRotationKeyPath];
animation.duration = 10;
// @( ) is fancy NSNumber literal syntax ...
animation.toValue = @(0.0);        // model value was already changed. End at that value
animation.byValue = @(angleToAdd); // start from - this value (it's toValue - byValue (see above))

// Add the animation. Once it completed it will be removed and you will see the value
// of the model layer which happens to be the same value as the animation stopped at.
[_myview.layer addAnimation:animation forKey:@"90rotation"];

Small disclaimer:

I didn't run this code but am fairly certain that it runs as it should and that I didn't do any typos. Correct me if I did. The entire discussion is still valid.

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • *minor correction: CGFloat currentAngle = [[self.imgView.layer valueForKeyPath:zRotationKeyPath] floatValue] – Iducool Jul 03 '13 at 06:09
  • I never new about removeOnCompletion having these issues. Thank you for the clear insight! Huge help! – daveMac Feb 27 '14 at 16:54
  • 2
    This will actually result in the animation starting from 0.0 - 90 or -90 and then rotating to 0.0. When the animation finishes, it will then jump to the model value. You'll actually want the to value to be the same as the model. i.e animation.toValue = @(currentAngle + angleToAdd) – sc0rp10n Feb 28 '14 at 01:39
  • @DavidRönnqvist How can I avoid using setting removedOnCompletion to know and delay the animation (animation setBeginTime:)? The problem is that for the duration of the delay, it shows the end (or to) value. – daveMac Apr 28 '14 at 16:55
  • @daveMac It sounds like you are looking for the "backwards" fill mode. That will cause the animation to show the "from value" before it starts (during the delay) – David Rönnqvist Apr 29 '14 at 12:03
  • @DavidRönnqvist But my understanding of using the fill modes is that removedOnCompletion has to be set to no. Is that correct, or does that only apply to forward fill mode? – daveMac Apr 29 '14 at 13:56
  • @daveMac I urge you to try for yourself and see how it works. Fill mode is separate from removed on completion. It's only that a forward fill mode isn't very meaningful on its own, since the animation is removed and here is nothing to fill – David Rönnqvist Apr 29 '14 at 18:00
  • @DavidRönnqvist Planning to try, I just wanted your expertise. Thanks! – daveMac Apr 29 '14 at 18:59
  • is this code being continuously called or it is only called once? – apinho Oct 15 '14 at 14:53
6

pass incremental value of angle see my code

static int imgAngle=0;
- (void)doAnimation
{
  CABasicAnimation *animation = [CABasicAnimation   animationWithKeyPath:@"transform.rotation.z"];
   animation.duration = 5;
  animation.additive = YES;
  animation.removedOnCompletion = NO;
  animation.fillMode = kCAFillModeForwards;
  animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle)];
  animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle+90)];
 [self.imgView.layer addAnimation:animation forKey:@"90rotation"];

  imgAngle+=90;
  if (imgAngle>360) {
      imgAngle = 0;
  }
}

Above code is just for idea. Its not tested

Iducool
  • 3,543
  • 2
  • 24
  • 45
  • 5
    `animation.removedOnCompletion = NO;` is a bad idea as you will pollute the layer with many animation instances if you run the code several times. See the most up-voted answer by @DavidRönnqvist for a better solution. – Ricardo Sanchez-Saez Jan 06 '15 at 15:21