164

I am rotating a CALayer and trying to stop it at its final position after animation is completed.

But after animation completes it resets to its initial position.

(xcode docs explicitly say that the animation will not update the value of the property.)

any suggestions how to achieve this.

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Nilesh Ukey
  • 5,488
  • 3
  • 20
  • 22
  • 4
    This is one of those strange SO questions where ***almost all of the answers are utterly wrong - just totally WRONG***. You very simply look at `.presentation()` to get the "final, seen" value. Search down for the correct answers below which explain it's done with the presentation layer. – Fattie Aug 27 '19 at 19:22
  • The best answer on this thread is this: https://stackoverflow.com/a/50668490/1101099 - the top answers may "work" but they are not the recommended solution. Make sure you understand the Core Animation programming model before copy/pasting! https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html – bitops Jan 22 '22 at 23:26

15 Answers15

311

Here's the answer, it's a combination of my answer and Krishnan's.

cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;

The default value is kCAFillModeRemoved. (Which is the reset behavior you're seeing.)

halfer
  • 19,824
  • 17
  • 99
  • 186
Nilesh Ukey
  • 5,488
  • 3
  • 20
  • 22
  • 7
    This is probably NOT the correct answer - see the comment on Krishnan's answer. You usually need this AND Krishnan's; just one or the other won't work. – Adam Jun 04 '12 at 12:56
  • The kCAFillModeForwards was the missing piece to what I was doing. – brynbodayle Mar 06 '13 at 22:56
  • important: you will NOT be able to subsequently change the values changed in your animation (they will will sit on the presentation layer and cover any changes to the underlying layer). I recommend instead setting final values in a completion block (i.e. set yourself as delegate and implement the callback) – Sam Oct 30 '13 at 19:31
  • 22
    This isn't the correct answer, see @Leslie Godwin's answer below. – Mark Ingram Dec 05 '13 at 09:26
  • 6
    @Leslie solution is better because it stores the final value in the layer and not in the animation. – Matthieu Rouif Aug 07 '14 at 15:33
  • 3
    Be careful when you are setting removedOnCompletion to NO. If you are assigning the animation delegate to "self", your instance will be retained by the animation. So, after calling removeFromSuperview if you forget to remove/destroy the animation by yourself, dealloc for instance will not be called. You'll have a memory leak. – emrahgunduz Jan 22 '15 at 14:07
  • 2
    This 200+ point answer is totally, completely, utterly incorrect. – Fattie Aug 27 '19 at 19:23
  • Although this answer works in many cases, Ayman Ibrahim's answer is the correct one... – horseshoe7 Nov 06 '20 at 08:38
  • This is just plainly wrong. https://stackoverflow.com/a/16327089 is the correct way. – Frizlab Jul 29 '22 at 18:35
90

The problem with removedOnCompletion is the UI element does not allow user interaction.

I technique is to set the FROM value in the animation and the TO value on the object. The animation will auto fill the TO value before it starts, and when it's removed will leave the object at it's correct state.

// fade in
CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: @"opacity"];
alphaAnimation.fillMode = kCAFillModeForwards;

alphaAnimation.fromValue = NUM_FLOAT(0);
self.view.layer.opacity = 1;

[self.view.layer addAnimation: alphaAnimation forKey: @"fade"];
Leslie Godwin
  • 2,601
  • 26
  • 18
  • 8
    Doesn't work if you set a start delay. e.g. `alphaAnimation.beginTime = 1;` Also, the `fillMode` isn't needed as far as I can tell...? – Sam Oct 30 '13 at 18:38
  • otherwise, this is a good answer, the accepted answer won't let you change a value after the animation completes... – Sam Oct 30 '13 at 18:39
  • 2
    i should also note that `beginTime` is not relative, you should use e.g.: `CACurrentMediaTime()+1;` – Sam Oct 30 '13 at 20:17
  • It's 'its' not "it's" — its-not-its.info. Sorry if it was just a typo. – fatuhoku Apr 28 '14 at 17:06
  • 7
    correct answer, but not need `fillMode`, see more in [Prevent Layers From Snapping Back to Original Values When Using Explicit CAAnimations](http://oleb.net/blog/2012/11/prevent-caanimation-snap-back/) – likid1412 Feb 04 '16 at 12:06
  • yes, this was the solution. basically if you only animate a property, you are not changing the underlying data, so it "thinks" it is still at the original from value. Setting the toValue on the layer just before you add the animation for that layer will see it animate correctly and can be animated again to an arbitrary toValue. – horseshoe7 Nov 06 '20 at 08:36
21

Core animation maintains two layer hierarchies: the model layer and the presentation layer. When the animation is in progress, the model layer is actually intact and keeps it initial value. By default, the animation is removed once the it's completed. Then the presentation layer falls back to the value of the model layer.

Simply setting removedOnCompletion to NO means the animation won't be removed and wastes memory. In addition, the model layer and the presentation layer won't be synchronous any more, which may lead to potential bugs.

So it would be a better solution to update the property directly on the model layer to the final value.

self.view.layer.opacity = 1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];

If there's any implicit animation caused by the first line of above code, try to turn if off:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.view.layer.opacity = 1;
[CATransaction commit];

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];
Lizhen Hu
  • 814
  • 10
  • 16
  • 2
    Thank you! This really should be the accepted answer, as it correctly explains this functionality instead of just using a band-aid to make it work – bplattenburg Dec 02 '19 at 18:27
  • This is the best answer I have found on this thread since it answers the question based on a true understanding of how Core Animation works under the hood. It's not that the accepted answer is "wrong" as such but it is not the recommended first solution. – bitops Jan 22 '22 at 23:26
16

Set the following property:

animationObject.removedOnCompletion = NO;
RK-
  • 12,099
  • 23
  • 89
  • 155
14

You can simply set the key of CABasicAnimation to position when you add it to the layer. By doing this, it will override implicit animation done on the position for the current pass in the run loop.

CGFloat yOffset = 30;
CGPoint endPosition = CGPointMake(someLayer.position.x,someLayer.position.y + yOffset);

someLayer.position = endPosition; // Implicit animation for position

CABasicAnimation * animation =[CABasicAnimation animationWithKeyPath:@"position.y"]; 

animation.fromValue = @(someLayer.position.y);
animation.toValue = @(someLayer.position.y + yOffset);

[someLayer addAnimation:animation forKey:@"position"]; // The explicit animation 'animation' override implicit animation

You can have more information on 2011 Apple WWDC Video Session 421 - Core Animation Essentials (middle of the video)

yageek
  • 4,115
  • 3
  • 30
  • 48
  • 2
    This looks like the correct way of doing this. Animation is naturally removed (default), and once animation is complete, it returns to the values set before animation started, specifically this line: `someLayer.position = endPosition;`. And thanks for the ref to the WWDC! – bauerMusic Jan 05 '16 at 05:00
13

This works:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3

someLayer.opacity = 1 // important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

Core animation shows a 'presentation layer' atop your normal layer during the animation. So set the opacity (or whatever) to what you want to be seen when the animation finishes and the presentation layer goes away. Do this on the line before you add the animation to avoid a flicker when it completes.

If you want to have a delay, do the following:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3
animation.beginTime = someLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) + 1
animation.fillMode = kCAFillModeBackwards // So the opacity is 0 while the animation waits to start.

someLayer.opacity = 1 // <- important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

Finally, if you use 'removedOnCompletion = false' it'll leak CAAnimations until the layer is eventually disposed - avoid.

Chris
  • 39,719
  • 45
  • 189
  • 235
12

A CALayer has a model layer and a presentation layer. During an animation, the presentation layer updates independently of the model. When the animation is complete, the presentation layer is updated with the value from the model. If you want to avoid a jarring jump after the animation ends, the key is to keep the two layers in sync.

If you know the end value, you can just set the model directly.

self.view.layer.opacity = 1;

But if you have an animation where you don't know the end position (e.g. a slow fade that the user can pause and then reverse), then you can query the presentation layer directly to find the current value, and then update the model.

NSNumber *opacity = [self.layer.presentationLayer valueForKeyPath:@"opacity"];
[self.layer setValue:opacity forKeyPath:@"opacity"];

Pulling the value from the presentation layer is also particularly useful for scaling or rotation keypaths. (e.g. transform.scale, transform.rotation)

Jason Moore
  • 7,169
  • 1
  • 44
  • 45
12

So my problem was that I was trying to rotate an object on pan gesture and so I had multiple identical animations on each move. I had both fillMode = kCAFillModeForwards and isRemovedOnCompletion = false but it didn't help. In my case, I had to make sure that the animation key is different each time I add a new animation:

let angle = // here is my computed angle
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.toValue = angle
rotate.duration = 0.1
rotate.isRemovedOnCompletion = false
rotate.fillMode = CAMediaTimingFillMode.forwards

head.layer.add(rotate, forKey: "rotate\(angle)")
Pedro Paulo Amorim
  • 1,838
  • 2
  • 27
  • 50
sunshinejr
  • 4,834
  • 2
  • 22
  • 32
12

just put it inside your code

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;
msk_sureshkumar
  • 343
  • 4
  • 10
9

Without using the removedOnCompletion

You can try this technique:

self.animateOnX(item: shapeLayer)

func animateOnX(item:CAShapeLayer)
{
    let endPostion = CGPoint(x: 200, y: 0)
    let pathAnimation = CABasicAnimation(keyPath: "position")
    //
    pathAnimation.duration = 20
    pathAnimation.fromValue = CGPoint(x: 0, y: 0)//comment this line and notice the difference
    pathAnimation.toValue =  endPostion
    pathAnimation.fillMode = kCAFillModeBoth

    item.position = endPostion//prevent the CABasicAnimation from resetting item's position when the animation finishes

    item.add(pathAnimation, forKey: nil)
}
Ayman Ibrahim
  • 1,359
  • 15
  • 24
  • 3
    This seems to be the best functioning answer – Michael Ramos Aug 22 '17 at 14:00
  • Agreed. Note @ayman's comment that you *must* define the .fromValue property to the start value. Otherwise, you will have overwritten the start value in the model, and there will be animation. – Collierton Oct 23 '18 at 22:02
  • This one and @jason-moore 's answer are better than the accepted answer. In the accepted answer the animation is just paused but the new value is not actually set to the property. This can result in unwanted behavior. – laka Dec 13 '19 at 09:41
8

Simply setting fillMode and removedOnCompletion didn't work for me. I solved the problem by setting all of the properties below to the CABasicAnimation object:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.duration = 0.38f;
ba.fillMode = kCAFillModeForwards;
ba.removedOnCompletion = NO;
ba.autoreverses = NO;
ba.repeatCount = 0;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.85f, 0.85f, 1.0f)];
[myView.layer addAnimation:ba forKey:nil];

This code transforms myView to 85% of its size (3rd dimension unaltered).

4

@Leslie Godwin's answer is not really good, "self.view.layer.opacity = 1;" is done immediately (it takes about one second), please fix alphaAnimation.duration to 10.0, if you have doubts. You have to remove this line.

So, when you fix fillMode to kCAFillModeForwards and removedOnCompletion to NO, you let the animation remains in the layer. If you fix the animation delegate and try something like:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
 [theLayer removeAllAnimations];
}

...the layer restores immediately at the moment you execute this line. It's what we wanted to avoid.

You must fix the layer property before remove the animation from it. Try this:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
     if([anim isKindOfClass:[CABasicAnimation class] ]) // check, because of the cast
    {
        CALayer *theLayer = 0;
        if(anim==[_b1 animationForKey:@"opacity"])
            theLayer = _b1; // I have two layers
        else
        if(anim==[_b2 animationForKey:@"opacity"])
            theLayer = _b2;

        if(theLayer)
        {
            CGFloat toValue = [((CABasicAnimation*)anim).toValue floatValue];
            [theLayer setOpacity:toValue];

            [theLayer removeAllAnimations];
        }
    }
}
tontonCD
  • 320
  • 2
  • 6
2

The easiest solution is to use implicit animations. This will handle all of that trouble for you:

self.layer?.backgroundColor = NSColor.red.cgColor;

If you want to customize e.g. the duration, you can use NSAnimationContext:

    NSAnimationContext.beginGrouping();
    NSAnimationContext.current.duration = 0.5;
    self.layer?.backgroundColor = NSColor.red.cgColor;
    NSAnimationContext.endGrouping();

Note: This is only tested on macOS.

I initially did not see any animation when doing this. The problem is that the layer of a view-backed layer does not implicit animate. To solve this, make sure you add a layer yourself (before setting the view to layer-backed).

An example how to do this would be:

override func awakeFromNib() {
    self.layer = CALayer();
    //self.wantsLayer = true;
}

Using self.wantsLayer did not make any difference in my testing, but it could have some side effects that I do not know of.

paxos
  • 877
  • 5
  • 11
1

It seems that removedOnCompletion flag set to false and fillMode set to kCAFillModeForwards doesn't work for me either.

After I apply new animation on a layer, an animating object resets to its initial state and then animates from that state. What has to be done additionally is to set the model layer's desired property according to its presentation layer's property before setting new animation like so:

someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:@"someAnimation"];
1

Here is a sample from playground:

import PlaygroundSupport
import UIKit

let resultRotation = CGFloat.pi / 2
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 300.0))
view.backgroundColor = .red

//--------------------------------------------------------------------------------

let rotate = CABasicAnimation(keyPath: "transform.rotation.z") // 1
rotate.fromValue = CGFloat.pi / 3 // 2
rotate.toValue = resultRotation // 3
rotate.duration = 5.0 // 4
rotate.beginTime = CACurrentMediaTime() + 1.0 // 5
// rotate.isRemovedOnCompletion = false // 6
rotate.fillMode = .backwards // 7

view.layer.add(rotate, forKey: nil) // 8
view.layer.setAffineTransform(CGAffineTransform(rotationAngle: resultRotation)) // 9

//--------------------------------------------------------------------------------

PlaygroundPage.current.liveView = view
  1. Create an animation model
  2. Set start position of the animation (could be skipped, it depends on your current layer layout)
  3. Set end position of the animation
  4. Set animation duration
  5. Delay animation for a second
  6. Do not set false to isRemovedOnCompletion - let Core Animation clean after the animation is finished
  7. Here is the first part of the trick - you say to Core Animation to place your animation to the start position (you set in step #2) before the animation has even been started - extend it backwards in time
  8. Copy prepared animation object, add it to the layer and start the animation after the delay (you set in step #5)
  9. The second part is to set correct end position of the layer - after the animation is deleted your layer will be shown at the correct place
adnako
  • 1,287
  • 2
  • 20
  • 30