9

I am trying to apply a rotation animation by number of degrees to a UIImageView and persist the rotation transformation in the completion block.

The problem that I am facing is that when the completion block is executed there is a visible flicker generated by passing from the end state of the animation to the completion block.

Here is the code that I am currently using:

if (futureAngle == currentAngle) {
    return;
}

float rotationAngle;
if (futureAngle < currentAngle) {
    rotationAngle = futureAngle - currentAngle;
}else{
    rotationAngle = futureAngle - currentAngle;
}

float animationDuration = fabs(rotationAngle) / 100;
rotationAngle = GLKMathDegreesToRadians(rotationAngle);

[CATransaction begin];
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.byValue = [NSNumber numberWithFloat:rotationAngle];
rotationAnimation.duration = animationDuration;
rotationAnimation.removedOnCompletion = YES;

[CATransaction setCompletionBlock:^{
    view.transform = CGAffineTransformRotate(view.transform, rotationAngle);
}];

[view.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
[CATransaction commit];
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Laur Stefan
  • 1,519
  • 5
  • 23
  • 50
  • When you say flicker, I assume you mean that at the end of the animation, that it momentarily returns to the initial state before returning back to the final state? This can be solved either by (a) setting the final `view.transform` _before_ you start the animation (and you no longer need the `completionBlock`); or (b) by setting the animation's `fillMode` to `kCAFillModeForwards` and set `removedOnCompletion` to `false`. – Rob Jun 28 '16 at 16:17
  • See http://oleb.net/blog/2012/11/prevent-caanimation-snap-back/ or http://stackoverflow.com/a/31010421/1271826 or http://stackoverflow.com/a/7690841/1271826 – Rob Jun 28 '16 at 16:23
  • @Rob can you provide an example for the suggestion regarding the completion block? I tried the fillMode to kCAFillModeForwards and set removedOnCompletion to false before creating the post and they didn't worked for me. – Laur Stefan Jun 29 '16 at 09:25

2 Answers2

24

When you say flicker, I assume you mean that at the end of the animation, that it momentarily returns to the initial state before returning back to the final state? This can be solved either by

  • setting the final view.transform before you start the animation (and you no longer need the completionBlock);
  • by setting the animation's fillMode to kCAFillModeForwards and set removedOnCompletion to false.

Personally, I think setting the animated property to its destination value before you start the animation is the easiest way to do this.

Thus:

- (void)rotate:(UIView *)view by:(CGFloat)delta {
    float animationDuration = 2.0;
    CGFloat currentAngle = self.angle;                                                    // retrieve saved angle
    CGFloat nextAngle = self.angle + delta;                                               // increment it
    self.angle = nextAngle;                                                               // save new value

    view.transform = CGAffineTransformMakeRotation(nextAngle);                            // set property to destination rotation

    CABasicAnimation *rotationAnimation;
    rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];  // now rotate
    rotationAnimation.fromValue = @(currentAngle);
    rotationAnimation.toValue = @(nextAngle);
    rotationAnimation.duration = animationDuration;
    rotationAnimation.removedOnCompletion = YES;
    [view.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}

Or, I think even easier, just adjust the transform:

- (void)rotate:(UIView *)view by:(CGFloat)delta {
    float animationDuration = 2.0;
    CGAffineTransform transform = view.transform;                                         // retrieve current transform
    CGAffineTransform nextTransform = CGAffineTransformRotate(transform, delta);          // increment it

    view.transform = nextTransform;                                                       // set property to destination rotation

    CABasicAnimation *rotationAnimation;
    rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];             // now rotate
    rotationAnimation.fromValue = [NSValue valueWithCGAffineTransform:transform];
    rotationAnimation.toValue = [NSValue valueWithCGAffineTransform:nextTransform];
    rotationAnimation.duration = animationDuration;
    rotationAnimation.removedOnCompletion = YES;
    [view.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Fascinating, it would seem that *if you are trying to do a UIView.animate at the same time* (say, just moving a constraint), the "fillMode approach" seems to be the answer ......... – Fattie Aug 17 '17 at 18:07
  • Thanks Rob! Somehow the first option doesn't work for me, but the second one does. – CodeBrew Apr 03 '21 at 19:26
6

I was seeing flickering even when using the suggested answer from Rob, but turns out it seems to just be a simulator bug. On real devices I dont see the flicker, if you have only been testing on simulator, try on a real device unless you want to waste hours of your life potentially like myself.

Fonix
  • 11,447
  • 3
  • 45
  • 74
  • 1
    Oh my god, you literally saved me tons of time. On simulator the animation flickers / blinks, but on real device it just works. It take me a while to realize that maybe there is no problem with my code, and the solution lies somewhere else. Very-very big thanks! – jason d Jul 08 '21 at 22:18