CAShapeLayerで折れ線グラフをアニメーション

#CGPathそのものをアニメーションしたいけれど……

iOSで自力で折れ線グラフを書こうと思うと、Core Graphicsの出番です。
ただ、今回やりたいのは

折れ線グラフを値の変更に応じて動的にアニメーションさせる

ということでしたので、少し調べてみました。

静的な折れ線グラフを書くだけであれば、方法はいくつかありますが、
私はグラフを表示する`UIView`の`drawRect`で以下のように書いていました。

```PlotView.swift
override func drawRect(rect: CGRect) {
    let context :CGContextRef! = UIGraphicsGetCurrentContext()
    CGContextSetLineWidth(context, 2.0)

    // 設定開始
    CGContextBeginPath(context)

    // グラフ上の座標を設定する
    CGContextMoveToPoint(context, 0, 100)
    CGContextAddLineToPoint(context, 50, 160)
    CGContextAddLineToPoint(context, 100, 120)
    
    // 描画
    CGContextStrokePath(context)
}
```

よくある、`CGContext〜`で始まる関数を使ってガリガリPathを繋いでいく方法です。
しかし、これをアニメーションしたい、となった途端に方法がわからなくなりました。
アニメーションするとしても、`UIView`や`CALayer`のアニメーションは、基本的にはそれらのプロパティ変化前と変化後の値を与えて、その変化をアニメーションさせるというもの。上記のPathそのものを動かすという形でのアニメーションのやり方が思いつきませんでした。

また、`CALayer`のプロパティも試しに見てみましたが、Pathを扱えるプロパティが見つかりません。
`CABasicAnimation`を使えそうだという見当はついても、それに渡す`keyPath`が分かりません。

#CALayerのサブクラスたち

調べているうちに、CALayerにはたくさんサブクラスがあることが分かりました。
合計14個がAppleのリファレンスには書かれています。

```
AVCaptureVideoPreviewLayer
AVPlayerLayer
AVSampleBufferDisplayLayer
AVSynchronizedLayer
CAEAGLLayer
CAEmitterLayer
CAGradientLayer
CAMetalLayer
CAReplicatorLayer
CAScrollLayer
CAShapeLayer
CATextLayer
CATiledLayer
CATransformLayer
```

`CABasicAnimation`に渡す`keyPath`は文字列で、基本的には`CALayer`のプロパティ名を書きますが、当然ながら、`CALayer`のサブクラス特有のプロパティを渡すこともできるわけです。もちろん、渡すことができても、そのプロパティでアニメーション可能だとは限りませんが。

上記のサブクラスのうち、`CAShapeLayer`が、Pathを扱うためのものです。プロパティにも、そのまんま`path`という名前のものがあります。これを使えば、Pathをまるごとアニメーションさせることができるかもしれない……

どういうことかというと、例えばCALayerで拡大・縮小をアニメーションさせたい場合、

```ScaleAnimController.swift
func scale() {
    // アニメーションオブジェクトを作成する
    let anim = CABasicAnimation(keyPath:"contentsScale")
    anim.fromValue = @1.0 //変化前の倍率
    anim.toValue = @3.0   //変化後の倍率
    anim.duration = 0.5
    
    // Layerにアニメーションを設定する
    let layer = CALayer()
    layer.addAnimation(anim, forKey:nil)
    
    // 本体ViewにLayerを追加
    // ここでアニメーションが開始される
    self.view.layer.addSubLayer(layer)
}
```

みたいな感じで、「変化前」「変化後」の値をCABasicAnimationに与えることで実現します。大変わかりやすいです。さて、`CAShapeLayer`に`path`プロパティがあるということは、この「変化前」「変化後」にPathを渡すことで、自動的にCore Animationがアニメーションを補完してくれるかもしれない、という推測が成り立つわけです。こんな感じです。

```PlotViewController.swift
func animPlot() {
    // アニメーションオブジェクトを作成する
    let anim = CABasicAnimation(keyPath:"path")
    anim.fromValue = 変化前のPath
    anim.toValue = 変化後のPath
    anim.duration = 0.5
    
    // Layerにアニメーションを設定する
    let layer = CALayer()
    layer.addAnimation(anim, forKey:nil)
    
    // 本体ViewにLayerを追加
    // ここでアニメーションが開始される
    self.view.layer.addSubLayer(layer)
}
```

そして`CAShapeLayer`の`path`プロパティの型は`CGPathRef`ですので、あらかじめ作っておいた`CGPathRef`を渡せば良いのでは、という話です。

```PlotViewController.swift
func animPlot() {
    // 変化前のグラフのPathを作成する
    let path1 = UIBezierPath()    
    path1.moveToPoint(CGPoint(x:0, y:100))
    path1.addLineToPoint(CGPoint(x:50, y:160))
    path1.addLineToPoint(CGPoint(x:100, y:120))
    
    // 変化後のグラフのPathを作成する
    let path2 = UIBezierPath()    
    path2.moveToPoint(CGPoint(x:0, y:80))
    path2.addLineToPoint(CGPoint(x:50, y:200))
    path2.addLineToPoint(CGPoint(x:100, y:90))

    // アニメーションオブジェクトを作成する
    let anim = CABasicAnimation(keyPath:"path")
    anim.fromValue = path1 // 変化前のPath
    anim.toValue = path2   // 変化後のPath
    anim.duration = 0.5
    
    // Layerにアニメーションを設定する
    let layer = CALayer()
    layer.addAnimation(anim, forKey:nil)
    
    // 本体ViewにLayerを追加
    // ここでアニメーションが開始される
    self.view.layer.addSubLayer(layer)
}
```

ドキドキしながら試してみましたが、見事に自分の求めていたアニメーションの動きになりました！！
グラフであれば、元々の値が変化したとしても、グラフ上の点の数はだいたい同じでしょうし、補完もこちらの意図と異なる結果になる余地が少ないので、だいたいの場合において満足いく結果になるのではと思います。ただ、`CABasicAnimation`の変化前と変化後には（論理的には）任意のPathを渡すことができるはずなので、全然異なる種類のPathからPathへのアニメーション時に、どのような補完がなされるのかは未知数です（どなたか試してみてくださいな）。

#追記
付け加えると、上記のコードをきちんと読んでいただいた型はお気づきでしょうが、`path`プロパティは`CGPathRef`型だと書いておきながら、実は`UIBezierPath`を渡しています。内部的に`UIBezierPath`が`CGPathRef`をラップするような形で保持していそうだという推測はできますが、これでうまくアニメーションできるというのは、なかなか不思議です。ま、便利なのでこのままいきます。