iOS, Restarting animation when coming out of the background

25,674

Solution 1

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.

Solution 2

You can add an observer in your class for UIApplicationWillEnterForegroundNotification:

- (void)addNotifications { 
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; 
}

- (void)applicationWillEnterForeground { 
    [self animate]; 
}

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

It is important to set the begin state of the animation (and don't forget to remove the notification observer)

Solution 3

There is a better soloution here than restarting whole animation each time you come from background.

For Swift 3 you can subclass this class:

class ViewWithPersistentAnimations : UIView {
    private var persistentAnimations: [String: CAAnimation] = [:]
    private var persistentSpeed: Float = 0.0

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    func commonInit() {
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func didBecomeActive() {
        self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
        self.persistentAnimations.removeAll()
        if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
            self.layer.resume()
        }
    }

    func willResignActive() {
        self.persistentSpeed = self.layer.speed

        self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
        self.persistAnimations(withKeys: self.layer.animationKeys())
        self.layer.speed = self.persistentSpeed //restore original speed

        self.layer.pause()
    }

    func persistAnimations(withKeys: [String]?) {
        withKeys?.forEach({ (key) in
            if let animation = self.layer.animation(forKey: key) {
                self.persistentAnimations[key] = animation
            }
        })
    }

    func restoreAnimations(withKeys: [String]?) {
        withKeys?.forEach { key in
            if let persistentAnimation = self.persistentAnimations[key] {
                self.layer.add(persistentAnimation, forKey: key)
            }
        }
    }
}

extension CALayer {
    func pause() {
        if self.isPaused() == false {
            let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
            self.speed = 0.0
            self.timeOffset = pausedTime
        }
    }

    func isPaused() -> Bool {
        return self.speed == 0.0
    }

    func resume() {
        let pausedTime: CFTimeInterval = self.timeOffset
        self.speed = 1.0
        self.timeOffset = 0.0
        self.beginTime = 0.0
        let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        self.beginTime = timeSincePause
    }
}

It will take care of pausing all your animations in current state and re-adding it when app comes from background - without resetting them.

Gist: https://gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128

Solution 4

You'll have to pause the animation when your app goes into the background and resume it when it becomes active again. You can find some sample code here that describes how to pause and resume an animation.

Solution 5

@Grzegorz Krukowski's answer worked like a charm for me. I just wanted to add that if you don't need to resume the animation from where it left off, but simulate it kept running on the background, you can simply omit these lines:

let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
self.beginTime = timeSincePause

from the resume() function in CALayer extension, and leave beginTime set to 0.0.

Also, you can manually call willResignActive and didBecomeActive methods for animations you want to resume when ViewController disappears, even if the app didn't go to the background.

Share:
25,674

Related videos on Youtube

Stultus
Author by

Stultus

iOS, Web SOreadytohelp

Updated on July 09, 2022

Comments

  • Stultus
    Stultus almost 2 years

    when my app comes out of the background the animation has stopped. which is normal. but i want to restart my animation from the current state. how do i do that without my snapping all over the place.

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

    i tried putting a set frame in front of the animation but that just makes it snap.

    [bg setFrame:CGRectMake(0, 0, 1378, 1005)];
    

    any ideas?

    • danny_23
      danny_23 about 13 years
      Hi. Could you explain a bit about what kind of effect you expect form that code? I think that "current state" is the current state of running (in-progress) animation. And I suppose that CA Framework doesn't care about remembering the state where the animation was interrupted. Are you trying to write a cycling animation that can be stopped and restarted again from where it was stopped without playback to initial position?
    • Stultus
      Stultus about 13 years
      hi. it's a kind of ken burns effect.
    • Stultus
      Stultus about 13 years
      and the current state is the state the object is in at the moment. So it takes in account all the animation currently active on the object
  • Deepak Danduprolu
    Deepak Danduprolu about 13 years
    +1 Captures the basic idea. Although, in this case @Stultus needn't set up a delegate and code it in animationDidStop:finished:. He can implement the finish: block of his animateWithDuration:delay:options:animations:finished: method. The block has a finished argument which will tell him if the animation has been interrupted. Rest of it should be like you mentioned.
  • Tricertops
    Tricertops almost 10 years
    +1 Notifications are better way to receive this kind of event, than using application delegate.
  • Lee Fastenau
    Lee Fastenau over 9 years
    +1 This definitely is the more flexible and elegant solution for restarting looping animations. The UIApplicationDelegate shouldn't have to care about restarting MYWaitCycleView's spinning chicken animation. @Ramiro's code is perfectly happy sitting in a UIViewController or UIView subclass, close to the thing being animated.
  • Jonny
    Jonny over 7 years
    I've used this method for years, but now ran into an issue where animations still stop and aren't restarted (they should be running indefinitely in my case); it happens when tapping the alert asking the user for approval of remote (and/or local) push alerts. I've yet to find a solution. Btw I use the notification UIApplicationDidBecomeActiveNotification instead of UIApplicationWillEnterForegroundNotification.
  • Lance Samaria
    Lance Samaria about 5 years
    This worked but what I had to do was remove the CALayer (I used a gradientLayer) that had the animation and then add it all over again. Other then that my animation would pause when coming back to the foreground. I explained it at the bottom of this answer stackoverflow.com/a/55883132/4833705
  • Jonatan Liljedahl
    Jonatan Liljedahl over 3 years
    This used to work well for me before, but at some point (maybe iOS 10?) I can't get the persisted animations to restore. Does anyone know what might have happened and if there's a solution for iOS 10 and later?
  • Honghao Zhang
    Honghao Zhang about 2 years
    Copied from developer.apple.com/library/archive/qa/qa1673/_index.html but didn't mention it. The method name is also misleading