Search code examples
ioscore-animation

Restoring animation where it left off when app resumes from background


I have an endlessly looping CABasicAnimation of a repeating image tile in my view:

a = [CABasicAnimation animationWithKeyPath:@"position"];
a.timingFunction = [CAMediaTimingFunction 
                      functionWithName:kCAMediaTimingFunctionLinear];
a.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
a.toValue = [NSValue valueWithCGPoint:CGPointMake(image.size.width, 0)];
a.repeatCount = HUGE_VALF;
a.duration = 15.0;
[a retain];

I have tried to "pause and resume" the layer animation as described in Technical Q&A QA1673.

When the app enters background, the animation gets removed from the layer. To compensate I listen to UIApplicationDidEnterBackgroundNotification and call stopAnimation and in response to UIApplicationWillEnterForegroundNotification call startAnimation.

- (void)startAnimation 
{
    if ([[self.layer animationKeys] count] == 0)
        [self.layer addAnimation:a forKey:@"position"];

    CFTimeInterval pausedTime = [self.layer timeOffset];
    self.layer.speed = 1.0;
    self.layer.timeOffset = 0.0;
    self.layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = 
      [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    self.layer.beginTime = timeSincePause;
}

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

The problem is that it starts again at the beginning and there is ugly jump from last position, as seen on app snapshot the system took when application did enter background, back to the start of the animation loop.

I can not figure out how to make it start at last position, when I re-add the animation. Frankly, I just don't understand how that code from QA1673 works: in resumeLayer it sets the layer.beginTime twice, which seems redundant. But when I've removed the first set-to-zero, it did not resume the animation where it was paused. This was tested with simple tap gesture recognizer, that did toggle the animation - this is not strictly related to my issues with restoring from background.

What state should I remember before the animation gets removed and how do I restore the animation from that state, when I re-add it later?


Solution

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