10

How can I have one CABasicAnimation to run after the other one has finishes? In other words sequentially. I've added the begin time of the second animation, however it seems that the second animation doesn't gets executed:

CABasicAnimation * appearance =[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    appearance.duration = 0.5;
    appearance.fromValue = [NSNumber numberWithFloat:0];
    appearance.toValue = [NSNumber numberWithFloat:340];
    appearance.repeatCount = 1;
    appearance.fillMode = kCAFillModeForwards;
    appearance.removedOnCompletion = NO;
    [notif.layer addAnimation:appearance forKey:@"transform.translation.y"];



    CABasicAnimation * theAnimation=[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    theAnimation.duration = 0.5;
    theAnimation.fromValue = [NSNumber numberWithFloat:0];
    theAnimation.toValue = [NSNumber numberWithFloat:10];
    theAnimation.repeatCount = 3;
    theAnimation.autoreverses = YES;
    theAnimation.fillMode = kCAFillModeForwards;
    theAnimation.removedOnCompletion = NO;
    theAnimation.beginTime = appearance.beginTime + appearance.duration;
    [notif.layer addAnimation:theAnimation forKey:@"transform.translation.y"];

Any idea why?

adit
  • 32,574
  • 72
  • 229
  • 373
  • You simply have to run both but set the delay on the second one to be the length of the first one. NOTE THAT in rare complex cases (where you don't know the length of the first one), use `-animationDidStop:finished:` as @ChrisParker points out. – Fattie May 03 '23 at 18:29

5 Answers5

15

Updated Code, this will work!

1st animation

CABasicAnimation * appearance =[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    [appearance setValue:@"animation1" forKey:@"id"];
    appearance.delegate = self;
    [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:)];
    appearance.duration = 0.5;
    appearance.fromValue = [NSNumber numberWithFloat:0];
    appearance.toValue = [NSNumber numberWithFloat:340];
    appearance.repeatCount = 1;
    appearance.fillMode = kCAFillModeForwards;
    appearance.removedOnCompletion = NO;
    [notif.layer addAnimation:appearance forKey:@"transform.translation.y"];

2nd Animation:

    - (void)animationDidStop:(CAAnimation *)theAnimation2 finished:(BOOL)flag {
    if([[theAnimation2 valueForKey:@"id"] isEqual:@"animation1"]) {
    CABasicAnimation * theAnimation=[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    theAnimation.duration = 0.5;
    theAnimation.fromValue = [NSNumber numberWithFloat:0];
    theAnimation.toValue = [NSNumber numberWithFloat:10];
    theAnimation.repeatCount = 3;
    theAnimation.autoreverses = YES;
    theAnimation.fillMode = kCAFillModeForwards;
    theAnimation.removedOnCompletion = NO;
    theAnimation.beginTime = appearance.beginTime + appearance.duration;
    [notif.layer addAnimation:theAnimation forKey:@"transform.translation.y"];
}

}

This is how the 2nd animation would fire after the 1st one finishes.

SimplyKiwi
  • 12,376
  • 22
  • 105
  • 191
  • this didn't work.. the animationDidStop was called infinitely – adit Oct 17 '11 at 01:57
  • If you've got a loop, are you sure you didn't set the delegate of the second animation such that the second animation is also calling animationDidStop? – isaac Oct 17 '11 at 02:48
  • if you looked at the code it self, it doesn't compile.. you're declaring a variable theAnimation in which the function parameter name is theAnimation as well – adit Oct 17 '11 at 04:16
  • Whoops, I fixed it above, hard to see when not using Xcode! :/ Also, up-vote too on my answer if you like it :P – SimplyKiwi Oct 17 '11 at 20:14
  • Works great. Thanks! Btw, I [wrote a µcategory on CABasicAnimation](https://gist.github.com/steakknife/5c55520a22b715438633) to flip `fromValue`<-->`toValue`. –  Aug 07 '14 at 00:22
  • 1
    It's 2016 now. Is this still the best solution to perform animations sequentially? – durazno Apr 09 '16 at 06:17
  • @durazno - yes, Apple have done nothing to improve CAAnimation in 10-15 years. – Fattie May 03 '23 at 18:28
2

The simplest thing to do is set up a delegate for the first animation and in that object implement -animationDidStop:finished:. In that method, touch off the second animation.

Chris Parker
  • 1,252
  • 8
  • 7
1

The reason your animation in not running according to plan is due to the way you have coded up & used Core Animation. Core animations are all GPU powered. CA infact handles couple of things very well to render animations optimally.

One element of it is the raw parallel processing power of modern graphics cards, and another is the fact that Core Animation can cache the contents of a CALayer on the card so that your code doesn't need to constantly redraw it. This, by the way, is part of the reason the iPhone UI is so incredibly fast on some relatively modest hardware. Core Animation can automatically take advantage of a multi-core Mac because the layer tree is rendered on a separate thread.

The last line is imp. CA's are running in a separate thread ( & not the main thread). So coming back to your problem, you have 2 blocks of CA's each trying to animate same transform.translation.y Both at the same time! How would this work?

So to solve this, either do -

  1. What someone suggested, put a delay for the second animation so that after the first animates, second kicks in only after the first os over.
  2. or Try to do the entire animation in one single block (again as someone suggested).

I just wanted to highlight the theory & reasoning behind the way Core Animation works. Hope this helps...

Srikar Appalaraju
  • 71,928
  • 54
  • 216
  • 264
0

It's really this simple ...

let basic_seconds = 2.0

blah.path = .. begin

let a1 = CABasicAnimation(keyPath: "path")
.. set from, to, etc
a1.duration = basic_seconds

let a2 = CABasicAnimation(keyPath: "path")
.. set from, to, etc
a2.beginTime = CACurrentMediaTime() + basic_seconds
a2.duration = 0.5

blah.add(a1, forKey: "a1")
blah.add(a2, forKey: "a2") // KEYS MUST BE DIFFERENT

CATransaction.begin()
blah.path = .. end 
CATransaction.commit()

You simply have to run both but set the DELAY on the second one to be the length of the first one.

NOTE THAT in rare complex cases (where you don't know the length of the first one), use -animationDidStop:finished: as @ChrisParker points out.

Don't forget that to set a delay in CA animations, you just use this silly code:

 .beginTime = CACurrentMediaTime() + .. some delay seconds ..

It's bizarre but that's the only mechanism to do sequential CA animations.

Fattie
  • 27,874
  • 70
  • 431
  • 719
-1

iOS 15, Swift 5.x

It is a bit nuts, but it was the only solution I could get to work, everything else I tried wanted to do things in parallel.

Note this doesn't run the second when the first finishes, it waits 4 seconds before running the second since it knows the first takes 4 seconds.

let foo = SCNAction.customAction(duration: 0) { (node,elapsedTime) in
  let animation = CABasicAnimation(keyPath:"morpher.weights[0]")
  animation.fromValue = 0.0
  animation.toValue = 1.0
  animation.autoreverses = true
  animation.repeatCount = 0
  animation.isRemovedOnCompletion = true
  animation.fillMode = .removed
  animation.duration = 2
  node.addAnimation(animation, forKey: nil)   
}

let delay = SCNAction.wait(duration: 4)
    
let foo2 = SCNAction.customAction(duration: 0) { (node,elapsedTime) in
  let animation2 =  CABasicAnimation(keyPath:"morpher.weights[1]")
  animation2.fromValue = 0.0
  animation2.toValue = 1.0
  animation2.autoreverses = true
  animation2.repeatCount = 0
  animation2.isRemovedOnCompletion = true
  animation2.fillMode = .removed
  animation2.duration = 2
  node.addAnimation(animation2, forKey: nil)
}
    
node.runAction(SCNAction.sequence([foo,delay,foo2]))
user3069232
  • 8,587
  • 7
  • 46
  • 87