Restoring Animations

Restoring Animations

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.