Restoring Animation Where It Left Off When App Resumes from Background

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.

iOS, Restarting animation when coming out of the background

well the answer of @dany_23 could work.

But I came across an other method that works just fine if you don't need to resume your animation but restart your animation, without the view or layer snapping when you reactivate the app.

in the

- (void)applicationWillResignActive:(UIApplication *)application

you call a method in your viewcontroller which implements the following code.

[view.layer removeAllAnimations];
// this following CGRect is the point where your view originally started
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];

and in the

- (void)applicationDidBecomeActive:(UIApplication *)application

you call a method in your viewcontroller that just starts the animation.
something like

[UIView animateWithDuration:60 delay:0 options:(UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
} completion:nil];

Hope this helps, Thanks to all who replied.

How to resume core animation when the app back to foreground

You can do this with UIKit's higher-level block based animation api. If you want a continuously rotating view with 20.0 second duration. You can with a function like:

    func animateImageView()
{
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .repeat, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi))
self.imageView.transform = transform
},
completion:
{ _ in
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi / 2.0))
self.imageView.transform = transform
})
})
}

This will just rotate the view by 180 degrees then once that is complete rotate it another 180 for 360 degree rotation. The .repeat option will cause it to repeat indefinitely. However, the animation will stop when the app is backgrounded. For that, we need to save the state of the presentation layer of the view being rotated. We can do that by storing the CGAffineTransform of the presentation layer and then setting that transform to the view being animated when the app comes back into the foreground. Here's an example with a UIImageView

class ViewController: UIViewController
{

@IBOutlet weak var imageView: UIImageView!
var imageViewTransform = CGAffineTransform.identity

override func viewDidLoad()
{
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
}

override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
animateImageView()
}

deinit
{
NotificationCenter.default.removeObserver(self)
}

func didEnterBackground()
{
imageViewTransform = imageView.layer.presentation()?.affineTransform() ?? .identity
}

func willEnterForeground()
{
imageView.transform = imageViewTransform
animateImageView()
}

func animateImageView()
{
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .repeat, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi))
self.imageView.transform = transform
},
completion:
{ _ in
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi / 2.0))
self.imageView.transform = transform
})
})
}
}

Which results in this:

Sample Image

How can I restart my block-based animation when the application comes to the foreground?

A friend figured out the problem, needed to enableAnimations on the view when it returns from the background

- (void)applicationWillEnterForeground:(UIApplication *)application
{
/*
Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
*/
[UIView enableAnimations:YES];
[[self viewController] animate];
......
}

then in the before the block animation need to removeAllAnimations and set layer.transform to Identity

hasStarted = YES;
for(UIButton * button in goldenBreakOutButtons){
for (UIView* view in button.subviews) {
if (wasStarted) {
[view.layer removeAllAnimations];
view.layer.transform = CATransform3DIdentity;
}
if ([view isKindOfClass:[UIImageView class]]) {
[UIView animateWithDuration:0.5f delay:0.0f
options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionRepeat
animations:^ {
[view.layer setTransform:CATransform3DMakeScale(1.3f, 1.3f, 1.0f)];
NSLog(@"animating");
}
completion:^(BOOL finished){
if (finished) {
NSLog(@"Completed");
}

}];
}
}
}

Prevent animation from completion when app closes

You can stop the animations of a UIView block animation by removing all the animations from the view's layer:

    self.carImage.layer.removeAllAnimations()

If this alone does not work, then you can also store the position of the view before stopping the animations and persist them while the app is in background:

    let currentPositionFrame = self.carImage.layer.presentation()?.frame
self.carImage.layer.removeAllAnimations()

Note that the currentPositionFrame will be a CGRect?, so you will have to handle the optionals, but it should be reasonable, as you may or may not have a previous position for the carImage when you open the screen anyway.

UPDATE:

To keep the state of the car image while in background, I'll outline the main parts of it in the view controller:

class MyViewController: UIViewController {
private var oldCarPosition: CGRect? = nil // here we'll store the position when the app goes to background

override func viewDidLoad() {
super.viewDidLoad()

// Other setup code

NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterBackground(_:)), name: UIApplication.willResignActiveNotification, object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}

deinit {
// Don't forget to unsubscribe from notifications!
NotificationCenter.default.removeObserver(self)
}


@objc func applicationWillEnterBackground(_ notification: Notification) {
self.oldCarPosition = self.carImage.layer.presentation()?.frame
}

@objc func applicationWillEnterForeground(_ notification: Notification) {
guard let oldPosition = self.oldCarPosition else { return }
self.carImage.frame = oldPosition
}
}


Related Topics



Leave a reply



Submit