adit
adit

Reputation: 33644

running CABasicAnimation sequentially

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?

Upvotes: 10

Views: 18458

Answers (5)

Fattie
Fattie

Reputation: 12631

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.

Upvotes: 0

user3069232
user3069232

Reputation: 8995

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]))

Upvotes: -1

SimplyKiwi
SimplyKiwi

Reputation: 12444

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.

Upvotes: 15

Srikar Appalaraju
Srikar Appalaraju

Reputation: 73608

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...

Upvotes: 1

Chris Parker
Chris Parker

Reputation: 1252

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.

Upvotes: 2

Related Questions