I have an endlessly looping CABasicAnimation
of a repeating image tile in my view:
a = [CABasicAnimation animationWithKeyPath:@\"position\"];
a
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:
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
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;
}
I can't comment so I will add it as an answer.
I used cclogg's solution but my app was crashing when the animation's view was removed from his superview, added again, and then going to background.
The animation was made infinite by setting animation.repeatCount
to Float.infinity
.
The solution I had was to set animation.removedOnCompletion
to false
.
It's very weird that it works because the animation is never completed. If anyone has an explanation, I like to hear it.
Another tip: If you remove the view from its superview. Don't forget to remove the observer by calling NSNotificationCenter.defaultCenter().removeObserver(...)
.
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"];
I was recognizing the gesture state like so:
// Perform action depending on the state
switch gesture.state {
case .changed:
// Some action
case .ended:
// Another action
// Ignore any other state
default:
break
}
All I needed to do was change the .ended
case to .ended, .cancelled
.
I use cclogg's solution to great effect. I also wanted to share some additional info that might help someone else, because it frustrated me for a while.
In my app I have a number of animations, some that loop forever, some that run only once and are spawned randomly. cclogg's solution worked for me, but when I added some code to
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
in order to do something when only the one-time animations were finished, this code would trigger when I resumed my app (using cclogg's solution) whenever those specific one-time animations were running when it was paused. So I added a flag (a member variable of my custom UIImageView class) and set it to YES in the section where you resume all the layer animations (resumeLayer
in cclogg's, analogous to Apple solution QA1673) to keep this from happening. I do this for every UIImageView that is resuming. Then, in the animationDidStop
method, only run the one-time animation handling code when that flag is NO. If it's YES, ignore the handling code. Switch the flag back to NO either way. That way when the animation truly finishes, your handling code will run. So like this:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
if (!resumeFlag) {
// do something now that the animation is finished for reals
}
resumeFlag = NO;
}
Hope that helps someone.
iOS will remove all animations when view disappears from the visible area (not only when app goes into background). To fix it I created custom CALayer
subclass and overrided 2 methods so the system doesn't remove animations - removeAnimation
and removeAllAnimations
:
class CustomCALayer: CALayer {
override func removeAnimation(forKey key: String) {
// prevent iOS to clear animation when view is not visible
}
override func removeAllAnimations() {
// prevent iOS to clear animation when view is not visible
}
func forceRemoveAnimation(forKey key: String) {
super.removeAnimation(forKey: key)
}
}
In the view where you want this layer to be used as main layer override layerClass
property:
override class var layerClass: AnyClass {
return CustomCALayer.self
}
To pause and resume animation:
extension CALayer {
func pause() {
guard self.isPaused() == false else {
return
}
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
func resume() {
guard self.isPaused() else {
return
}
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
self.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
}
func isPaused() -> Bool {
return self.speed == 0.0
}
}