Swift：再生スピードが変更可能なキーフレームアニメーション

# はじめに
[RunCat](https://kyomesuke.com/runcat/index_jp.html)のキーフレームアニメーションのコアな部分の実装について公開．
速度の変わるキーフレームアニメーション（いわゆるパラパラ漫画とかコマ撮りアニメ）の実装方法の文献がなく，非常に厄介だった．

# デモ
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67153/61d17fc7-446f-6232-4463-96ebadafcf7e.gif)
このデモのやつは普通に`NSView`の`layer`上でアニメーションしているので簡単だが，メニューバー（`NSStatusItem`）上で再生するとなると実際にはもっと手の込んだことが必要になる．

# ソース
`duration`を固定して，親レイヤーの`speed`を調整するというのがキモ
あとは`timeOffset`と`beginTime`の役割把握が重要

```swift:AnimationLayer.swift
import Cocoa

class AnimationLayer: CALayer {
    
    //キーフレームアニメーションをするやつ
    private var keyFrameAnimation: CAKeyframeAnimation!

    //何かしらの初期設定（実装によってはいらない）
    public func initialize() {
        self.masksToBounds = true
        self.contentsScale = 2.0
    }
    
    //キーフレームアニメーションの用意
    public func setSequence(_ sequence: [NSImage]) {
        keyFrameAnimation = CAKeyframeAnimation(keyPath: "contents")
        keyFrameAnimation.calculationMode = .discrete //パラパラ漫画形式にするために必須な設定
        keyFrameAnimation.fillMode = .forwards
        keyFrameAnimation.repeatCount = Float.infinity
        keyFrameAnimation.autoreverses = false
        keyFrameAnimation.isRemovedOnCompletion = false
        keyFrameAnimation.beginTime = 0.0
        keyFrameAnimation.values = sequence
        keyFrameAnimation.duration = Double(sequence.count)
    }
    
    //アニメーション開始
    public func startAnimate() {
        if keyFrameAnimation == nil { return }
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        self.add(keyFrameAnimation, forKey: "running")
        CATransaction.commit()
    }

    //アニメーションのスピード変更
    public func updateSpeed(_ speed: Float) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        self.timeOffset = self.convertTime(CACurrentMediaTime(), from: nil)
        self.beginTime = CACurrentMediaTime()
        self.speed = speed
        CATransaction.commit()
    }
    
}
```

```swift:ViewController.swift
import Cocoa

class ViewController: NSViewController {
    
    let animationLayer = AnimationLayer()
    var timer: Timer? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //アニメーションレイヤーを初期化して追加
        self.view.wantsLayer = true
        animationLayer.initialize()
        self.view.layer!.addSublayer(animationLayer)
        
        //アニメーションするためのコマ画像を用意する
        var icons = [NSImage]()
        for i in (1 ... 4) {
            let icon = NSImage(imageLiteralResourceName: "page" + String(i))
            icons.append(icon)
        }
        //フレームサイズと画像を設定してアニメーション開始
        animationLayer.frame = NSRect(x: 40, y: 40, width: self.view.bounds.width - 80, height: self.view.bounds.height - 80)
        animationLayer.setSequence(icons)
        animationLayer.startAnimate()

        //例として定期的にアニメーションをランダムなスピードに変更
        timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { (t) in
            let speed = Float.random(in: 1 ... 5)
            self.animationLayer.updateSpeed(speed)
        }
    }
    
    override func viewWillDisappear() {
        timer?.invalidate()
    }

    override var representedObject: Any? {
        didSet {
        }
    }

}
```

# 備考
一応この方法をベースとしてキーフレームアニメーションを行うことが可能なのだが，`Core Animation`は勝手にGPUを使える場合は使ってしまうらしく，発熱問題をユーザーから指摘された．GPUを使わずにCPUの消耗が小さい実装方法が知りたい．