11

I am trying to delay the animation of layer's opacity and position by 3 seconds using setBeginTime. I have called the layer boxLayer. The animation is going well however during the first 3 seconds (the layer is not supposed to show yet) the layer is displayed at its final position and opacity. It should not. Group animation does not resolve the issue. Could anyone help? See code below:

// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:@"opacity"];


// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];


// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];

// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];

// Add it to the layer
[boxLayer addAnimation:fader forKey:@"BigFade"];

// Maintain opacity to 1.0 JUST TO MAKE SURE IT DOES NOT GO BACK TO ORIGINAL OPACITY
[boxLayer setOpacity:endOpacity];


// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:@"position"];

// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];

// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];

// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];

// Add it to the layer
[boxLayer addAnimation:mover forKey:@"BigMove"];

// Maintain the end position at 400.0 450.0 OTHERWISE IT IS GOING BACK TO ORIGINAL POSITION
[boxLayer setPosition:CGPointMake(endX, endY)];
Armand
  • 435
  • 1
  • 11
  • 23
  • how about making a method like [self performSelector:@selector(methodname) withObject:nil afterDelay:3.0f]; or using sleep(); – Danny Lin Jan 30 '13 at 07:18
  • My problem is not the delayed animation but rather the fact that the layer is displayed before the delayed animation starts. – Armand Jan 30 '13 at 12:20

3 Answers3

18

The problem is that you're setting the boxLayer properties of position and of opacity to their end values. You need to:

  1. Set the boxLayer properties to their starting values, not their ending values (this is why it's starting in the ending position/opacity ... usually if the animation starts immediately, this isn't an issue, but because you're deferring the start, using the ending positions is problematic);

  2. For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).

Thus:

// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:@"opacity"];

// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];

// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];

// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];

// MAKE SURE IT DOESN'T CHANGE OPACITY BACK TO STARTING VALUE    
[fader setRemovedOnCompletion:NO];
[fader setFillMode:kCAFillModeForwards];

// Add it to the layer
[boxLayer addAnimation:fader forKey:@"BigFade"];

// SET THE OPACITY TO THE STARTING VALUE
[boxLayer setOpacity:startOpacity];

// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:@"position"];

// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];

// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];

// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];

// MAKE SURE IT DOESN'T MOVE BACK TO STARTING POSITION   
[mover setRemovedOnCompletion:NO];
[mover setFillMode:kCAFillModeForwards];

// Add it to the layer
[boxLayer addAnimation:mover forKey:@"BigMove"];

// SET THE POSITION TO THE STARTING POSITION
[boxLayer setPosition:CGPointMake(startX, startY)];

Personally, I think you're doing a lot of work for something that's done far more easily with block-based animation on the view (for the purposes of this demonstration, I'm assuming your boxLayer is a CALayer for a control called box). You don't need Quartz 2D, either, if you do it this way:

box.alpha = startOpacity;
box.frame = CGRectMake(startX, startY, box.frame.size.width, box.frame.size.height);

[UIView animateWithDuration:1.0
                      delay:3.0
                    options:0
                 animations:^{
                     box.alpha = endOpacity;
                     box.frame = CGRectMake(endX, endY, box.frame.size.width, box.frame.size.height);
                 }
                 completion:nil];
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    My problem is not the delayed animation per se. That works. My problem is that the layer to be animated (moved and faded in) appears during 3 seconds before it is animated. – Armand Jan 30 '13 at 12:17
  • 1
    Yep. Exactly as you said Rob. It is "starts visible for 3 sec, and then disappears and slowly fades back in and animates over the next second". I have created the layer before the block I posted. I figured it would have been too long to post everything. It's like I can either delay the layer animation while having it displayed for the first seconds or I can delay the display of the layer while no animation occurs. Funny. I'm using methods such as setBeginTime and Duration. Not autolayout or animateWithDuration. Any idea? – Armand Jan 30 '13 at 20:46
  • @user1915931 While I think it's much easier to use block based animation of the views, I've updated my answer with the `CABasicAnimation` answer, too, if you have your heart set on animating the layers. – Rob Jan 30 '13 at 22:16
  • Rob I'm open to use block based animation even though I've never done it before... I'll try to use your example and let you know. – Armand Jan 30 '13 at 22:36
  • Thanks a lot Rob!!! It works. I also appreciate your mention of block based animation. The reason why I'm stuck on layer is because I'm using a CATextLayer. It has text in it. The content changes from page to page. Plus I need to flip it on tap. So please feel free to let me know if there's any simpler way to achieve all this... Could block based animation can deal with embedded text previously pulled out from database, flipping motion? Thanks already :-) – Armand Jan 31 '13 at 01:11
  • @Armand let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/23672/discussion-between-rob-and-armand) – Rob Jan 31 '13 at 03:03
  • In my case, I apply a fade in animation of 3.0 seconds with a delay of 1.0 second (beginTime). When my view appear, the fade animation instead of starting 1 second late, it starts right away but at 2/3 of the animation.. (so the view is 67% opaque) then at 1 second, it jumps to 0%.. then reach 67% at the 3rd second. Kinda weird result.. – Van Du Tran Dec 17 '13 at 23:36
  • @VanDuTran That's very strange. Did you initiate this from the main queue? and you're using the block-based animation? Are you using auto-layout? You might want to post your own question on Stack Overflow, where you can share the full details of your scenario. – Rob Dec 18 '13 at 01:30
  • Oh okai, i might post it when i'll have the time. The animations are inside a custom UIView, there is no auto-layout and no block-based animation, just plain CABasicAnimation. However, the custom UIView is added to my view controller's view, which is auto-layout and the animation could be called in a block. My solution for now is to use "performSelectorWithDelay" – Van Du Tran Dec 18 '13 at 16:32
  • A block based animation's completion block won't get called when animation's interrupted if user leaves the app etc hence it's useless if you must call a method when animation ends. – durazno Apr 09 '16 at 04:55
3

For using beginTime you should make necessary configuration of your animation object and set fillMode to kCAFillModeBackwards like

zoomAnimation.fillMode = kCAFillModeBackwards;

That's said in Apple documentation:

Use the beginTime property to set the start time of an animation. Normally, animations begin during the next update cycle. You can use the beginTime parameter to delay the animation start time by several seconds. The way to chain two animations together is to set the begin time of one animation to match the end time of the other animation. If you delay the start of an animation, you might also want to set the fillMode property to kCAFillModeBackwards. This fill mode causes the layer to display the animation’s start value, even if the layer object in the layer tree contains a different value. Without this fill mode, you would see a jump to the final value before the animation starts executing. Other fill modes are available too.

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW2

Also, from Rob's answer:

For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).

It's a kind of controversial statement, because:

Once the animation is removed, the presentation layer will fall back to the values of the model layer, and since we’ve never modified that layer’s position, our spaceship reappears right where it started. There are two ways to deal with this issue:

The first approach is to update the property directly on the model layer. This is the recommended approach, since it makes the animation completely optional.

Alternatively, you can tell the animation to remain in its final state by setting its fillMode property to kCAFillModeForwards and prevent it from being automatically removed by setting removedOnCompletion to NO. However, it’s a good practice to keep the model and presentation layers in sync, so this approach should be used carefully.

From https://www.objc.io/issues/12-animations/animations-explained/

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
surfrider
  • 1,376
  • 14
  • 29
1

This article explains well why you shouldn't use removedOnCompletion with fillMode https://www.objc.io/issues/12-animations/animations-explained/

In my case I'm animating the layer of a view that functions as a navigation but delaying a bounce animation that is inside that view ; I NEED BOTH OF THESE POSITIONS UPDATED ON THE LAYER since it can be dismissed and then shown again. Using removedOnCompletion will not update the layer's value once the animation completes

The way I do it is update the layer in a CATransaction completion block

CATransaction.setCompletionBlock { 
     // update the layer value
 }

CATransaction.begin()

// setup and add your animation

CATransaction.commit()
dsieczko
  • 403
  • 3
  • 11