Search code examples
swiftmacoscore-animationappkit

How to prevent an NSImageView from loosing an animation when its parent NSToolbar is hidden/shown?


Xcode: 9.2.
macOS Target: 10.13

It appears that an NSImageView will loose any animations added to its layer when the parent NSToolbar is made hidden then subsequently shown.

enter image description here

Is there a way to instruct AppKit to be hands off/restore the state of the animation?

Example code

class WindowController: NSWindowController, CALayerDelegate {

static let spinAnimation: CAAnimation = {
    let basicAnimation = CABasicAnimation(keyPath:"transform.rotation")
    basicAnimation.fromValue = 2.0 * .pi
    basicAnimation.toValue = 0.0
    basicAnimation.duration = 1.0
    basicAnimation.repeatCount = Float.infinity

    return basicAnimation
}()

@IBOutlet weak var imageView: NSImageView! {
    didSet{
        let layer = CALayer()
        layer.contentsScale = 2.0
        layer.contentsGravity = "aspectFit"
        layer.contents = #imageLiteral(resourceName: "windmill")
        imageView.layer = layer
        imageView.wantsLayer = true
        imageView.layerContentsRedrawPolicy = .onSetNeedsDisplay
        imageView.layer?.delegate = self
        imageView.needsDisplay = true
    }
}

func display(_ layer: CALayer) {
    let frame = layer.frame
    layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    layer.frame = frame
}

override func windowDidLoad() {
    super.windowDidLoad()


    let key = "spinAnimation"

    self.imageView.layer?.add(WindowController.spinAnimation, forKey: key)

    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5)) {
        self.imageView.layer?.removeAnimation(forKey: key)
    }
}
}

Sample Xcode project on GitHub


Solution

  • Normally, an animation is considered “completed” when its layer is removed from an on-screen layer tree. By default, an animation is removed from its layer when the animation completes. AppKit removes the toolbar view (and hence all its subviews and their layers) from the window, so the animation is considered completed and removed from its layer.

    To keep the animation installed, you can set the animation's isRemovedOnCompletion to false.

    import Cocoa
    
    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        @IBOutlet weak var window: NSWindow!
        @IBOutlet var customItem: NSToolbarItem!
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
            let view = customItem.view!
            view.wantsLayer = true
            let layer = view.layer!
            let frame = layer.frame
            layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
            layer.frame = frame
    
            let animation = CABasicAnimation(keyPath: "transform.rotation")
            animation.fromValue = CGFloat(0)
            animation.toValue = 2 * CGFloat.pi
            animation.duration = 1
            animation.repeatCount = .infinity
            animation.isRemovedOnCompletion = false
            layer.add(animation, forKey: animation.keyPath)
        }
    
    }
    

    Result:

    demo