Restoring animation where it left off when app resumes from background

After quite a lot of searching and talks with iOS development gurus, it appears that QA1673 doesn't help when it comes to pausing, backgrounding, then moving to foreground. My experimentation even shows that delegate methods that fire off from animations, such as animationDidStop become unreliable.

Sometimes they fire, sometimes they don't.

This creates a lot of problems because it means that, not only are you looking at a different screen that you were when you paused, but also the sequence of events currently in motion can be disrupted.

My solution thus far has been as follows:

When the animation starts, I get the start time:

mStartTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

When the user hits the pause button, I remove the animation from the CALayer:

[layer removeAnimationForKey:key];

I get the absolute time using CACurrentMediaTime():

CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

Using the mStartTime and stopTime I calculate an offset time:

mTimeOffset = stopTime - mStartTime;

I also set the model values of the object to be that of the presentationLayer. So, my stop method looks like this:

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

- (void)stop
{
    const CALayer *presentationLayer = layer.presentationLayer;

    layer.bounds = presentationLayer.bounds;
    layer.opacity = presentationLayer.opacity;
    layer.contentsRect = presentationLayer.contentsRect;
    layer.position = presentationLayer.position;

    [layer removeAnimationForKey:key];

    CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    mTimeOffset = stopTime - mStartTime;
}

On resume, I recalculate what's left of the paused animation based upon the mTimeOffset. That's a bit messy because I'm using CAKeyframeAnimation. I figure out what keyframes are outstanding based on the mTimeOffset. Also, I take into account that the pause may have occurred mid frame, e.g. halfway between f1 and f2. That time is deducted from the time of that keyframe.

I then add this animation to the layer afresh:

[layer addAnimation:animationGroup forKey:key];

The other thing to remember is that you will need to check the flag in animationDidStop and only remove the animated layer from the parent with removeFromSuperlayer if the flag is YES. That means that the layer is still visible during the pause.

This method does seem very laborious. It does work though! I'd love to be able to simply do this using QA1673. But at the moment for backgrounding, it doesn't work and this seems to be the only solution.


It's surprising to see that this isn't more straightforward. I created a category, based on cclogg's approach, that should make this a one-liner.

CALayer+MBAnimationPersistence

Simply invoke MB_setCurrentAnimationsPersistent on your layer after setting up the desired animations.

[movingView.layer MB_setCurrentAnimationsPersistent];

Or specify the animations that should be persisted explicitly.

movingView.layer.MB_persistentAnimationKeys = @[@"position"];

Hey I had stumbled upon the same thing in my game, and ended up finding a somewhat different solution than you, which you may like :) I figured I should share the workaround I found...

My case is using UIView/UIImageView animations, but it's basically still CAAnimations at its core... The gist of my method is that I copy/store the current animation on a view, and then let Apple's pause/resume work still, but before resuming I add my animation back on. So let me present this simple example:

Let's say I have a UIView called movingView. The UIView's center is animated via the standard [UIView animateWithDuration...] call. Using the mentioned QA1673 code, it works great pausing/resuming (when not exiting the app)... but regardless, I soon realized that on exit, whether I pause or not, the animation was completely removed... and here I was in your position.

So with this example, here's what I did:

  • Have a variable in your header file called something like animationViewPosition, of type *CAAnimation**.
  • When the app exits to background, I do this:

    animationViewPosition = [[movingView.layer animationForKey:@"position"] copy]; // I know position is the key in this case...
    [self pauseLayer:movingView.layer]; // this is the Apple method from QA1673
    
    • Note: Those 2 ^ calls are in a method that is the handler for the UIApplicationDidEnterBackgroundNotification (similar to you)
    • Note 2: If you don't know what the key is (of your animation), you can loop through the view's layer's 'animationKeys' property and log those out (mid animation presumably).
  • Now in my UIApplicationWillEnterForegroundNotification handler:

    if (animationViewPosition != nil)
    {
        [movingView.layer addAnimation:animationViewPosition forKey:@"position"]; // re-add the core animation to the view
        [animationViewPosition release]; // since we 'copied' earlier
        animationViewPosition = nil;
    }
    [self resumeLayer:movingView.layer]; // Apple's method, which will resume the animation at the position it was at when the app exited
    

And that's pretty much it! It has worked for me so far :)

You can easily extend it for more animations or views by just repeating those steps for each animation. It even works for pausing/resuming UIImageView animations, ie the standard [imageView startAnimating]. The layer animation key for that (by the way) is "contents".

Listing 1 Pause and Resume animations.

-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}