Objective-C - CABasicAnimation applying changes after animation?

Solution 1:

When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.

When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.

To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.

So how do you do all this? The basic recipe looks like this:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];

I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.

I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:

[CATransaction begin]; {
    [CATransaction setCompletionBlock:^{
        [self.imageView removeFromSuperview];
    }];
    [self addPositionAnimation];
    [self addScaleAnimation];
    [self addOpacityAnimation];
} [CATransaction commit];

Solution 2:

CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.

Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.

With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.

Solution 3:

Heres an example in Swift that may help someone

It's an animation on a gradient layer. It's animating the .locations property.

The critical point as @robMayoff answer explains fully is that:

Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!

The following is a good example because the animation repeats endlessly.

When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"

var previousLocations: [NSNumber] = []
...

func flexTheColors() { // "flex" the color bands randomly
    
    let oldValues = previousTargetLocations
    let newValues = randomLocations()
    previousTargetLocations = newValues
    
    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    
    // AND NOW ANIMATE:
    CATransaction.begin()
    
    // and by the way, this is how you endlessly animate:
    CATransaction.setCompletionBlock{ [weak self] in
        if self == nil { return }
        self?.animeFlexColorsEndless()
    }
    
    let a = CABasicAnimation(keyPath: "locations")
    a.isCumulative = false
    a.autoreverses = false
    a.isRemovedOnCompletion = true
    a.repeatCount = 0

    a.fromValue = oldValues
    a.toValue = newValues
    
    a.duration = (2.0...4.0).random()
    
    theLayer.add(a, forKey: nil)
    CATransaction.commit()
}

The following may help clarify something for new programmers. Note that in my code I do this:

    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    
    // AND NOW ANIMATE:
    CATransaction.begin()
    ...set up the animation...
    CATransaction.commit()

however in the code example in the other answer, it's like this:

    CATransaction.begin()
    ...set up the animation...
    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    CATransaction.commit()

Regarding the position of the line of code where you "set the values, before animating!" ..

It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().

I only mention this as it may confuse new animators.