You may find yourself in a situation when iOS removes animations automatically. It occurs in two cases:
- when an application goes into the background, all animations are removed from all layers
- when a view is removed from the window, all animations are removed from it’s layer tree
To counteract the system we need to pause the animations and copy them over to a safe place. Next, when the right moment comes, we need to add them back to the layers and resume.
Pausing and resuming animations of a layer tree
Technical Q&A QA1673 shows us how to stop the passage of time in a layer tree.
extension CALayer {
/// Pauses animations in layer tree.
fileprivate func pause() {
let pausedTime = convertTime(CACurrentMediaTime(), from: nil)
speed = 0.0;
timeOffset = pausedTime;
}
/// Resumes animations in layer tree.
fileprivate func resume() {
let pausedTime = timeOffset;
speed = 1.0;
timeOffset = 0.0;
beginTime = 0.0;
let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime;
beginTime = timeSincePause;
}
}
Storing and restoring animations
Beside animation object we need to store two additional pieces of information, reference to CALayer
the animation was added to and the key that identifies it. This way we will know where to restore the animations and under which keys. A convenient way to model it would be [key : animation]
dictionary on CALayer
.
extension CALayer {
private static let association = ObjectAssociation<NSDictionary>()
private var animationsStorage: [String: CAAnimation] {
get { return CALayer.association[self] as? [String : CAAnimation] ?? [:] }
set { CALayer.association[self] = newValue as NSDictionary }
}
ObjectAssociation is a convenient way to utilize associated object objective-c feature.
Having the storage place we can finish storeAnimations
and restoreAnimations
methods with ease. Note that they are both recursive functions.
/// Returns a dictionary of copies of animations currently attached to the layer along with their's keys.
private var animationsForKeys: [String: CAAnimation] {
guard let keys = animationKeys() else { return [:] }
return keys.reduce([:], {
var result = $0
let key = $1
result[key] = (animation(forKey: key)!.copy() as! CAAnimation)
return result
})
}
/// Pauses the layer tree and stores it's animations.
func storeAnimations() {
pause()
depositAnimations()
}
/// Resumes the layer tree and restores it's animations.
func restoreAnimations() {
withdrawAnimations()
resume()
}
private func depositAnimations() {
animationsStorage = animationsForKeys
sublayers?.forEach { $0.depositAnimations() }
}
private func withdrawAnimations() {
sublayers?.forEach { $0.withdrawAnimations() }
animationsStorage.forEach { add($0.value, forKey: $0.key) }
animationsStorage = [:]
}
}
Storing and restoring animations automatically
So far we were able to use only extensions to add functionality to existing classes, but in order to be notified when a view is added to/removed from window we are forced to subclassing (at least I could not find a way to achieve this, window
property on UIView
is not KVO-compliant).
class AnimationPreservingView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
registerForNotifications()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
registerForNotifications()
}
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow == nil { layer.storeAnimations() }
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil { layer.restoreAnimations() }
}
}
extension AnimationPreservingView {
fileprivate func registerForNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(AnimationPreservingView.applicationWillResignActive),
name: .UIApplicationWillResignActive,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(AnimationPreservingView.applicationDidBecomeActive),
name: .UIApplicationDidBecomeActive,
object: nil)
}
@objc private func applicationWillResignActive() {
guard window != nil else { return }
layer.storeAnimations()
}
@objc private func applicationDidBecomeActive() {
guard window != nil else { return }
layer.restoreAnimations()
}
}
Once your views are embedded inAnimationPreservingView
their animations will be safe.
Navigate here to see the full project.