SwiftでCAAnimaton

SwiftでCAAnimatonを使ったカスタムUIを作ってみました。
![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/14751/09fcce30-7261-01fe-1e8b-d6ce2f66ccb0.gif)

コードは[こちら](https://github.com/yoshimin/SlitTabViewSample)です。

以下に今回使ったCAAnimatonのTipsを2点ほどメモしておきます。

## CALayerにアニメーションを設定
このタブビューのUIは以下のような2つのCALayerを使って作られています。

- タブの全体（tablayer）
![whole_tab.png](https://qiita-image-store.s3.amazonaws.com/0/14751/a34cc4fd-fe6c-8f0a-e0ed-0fa04dae4a64.png)

- 選択されているタブ（labelLayer）
![selected_tab.png](https://qiita-image-store.s3.amazonaws.com/0/14751/1923e551-2193-f9c7-2a5b-1890962dd1d5.png)

これらは `UIBezierPath` で切れ込みが入ったタブの形を描画し、 `CALayer` の `path` に設定して作っています。
タブがタップされた際は、この2つの `CALayer`の `path` を `CAAnimaton` でアニメーションさせます。
実装は以下のようになります。

```
// CALayerのpathをアニメーションさせたいのでkeyには "path" を指定します
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 0.25
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)

// アニメーションが終わっても元に戻らないようにremovedOnCompletionをfalseに設定しておきます
animation.removedOnCompletion = false

// タップされたタブに切れ込みの位置を動かした形をUIBezierPathで再描画してtoValueに設定
animation.toValue = tabLayerPath()
// tabLayerにアニメーションを追加します
tabLayer?.addAnimation(animation, forKey: "tabAnimation")

// 選択されたタブも同様にtoValueに値を設定してlabelLayerにアニメーションを追加します（animationは使いまわしてtoValueだけ書き換えました） 
animation.toValue = labelLayerPath()
labelLayer?.addAnimation(animation, forKey: "labelAnimation")
```


## アニメーション完了時のコールバックを設定する
次にコールバックの設定について書きます。
コールバックの中では以下の2つ処理を行います。

1. `tabLayer` と `labelLayer` から `CABasicAnimation` を取り除く
2. `tabLayer.path` と `labelLayer.path` に値をセット

**1.`tabLayer` と `labelLayer` から `CABasicAnimation` を取り除く**
上記の実装例でアニメーションが終わっても元に戻らないように `removedOnCompletion` を `false` に設定していましたが、これだと、`tabLayer` と `labelLayer` にいつまでも `CABasicAnimation` が残り続けてしまいます。
なので、アニメーションが完了したら取り除く必要があります。

**2.`tabLayer.path` と `labelLayer.path` に値をセット**
上記の実装例では `tabLayer` と `labelLayer` はアニメーションを設定されただけで、 `tabLayer.path` と `labelLayer.path` は実際には値が代入されていません。
なので、`tabLayer` と `labelLayer` から `CABasicAnimation` を取り除いてしまうとタブがアニメーション前の形に戻ってしまいます。
アニメーション完了のコールバックの中で `tabLayer.path` と `labelLayer.path` に値を代入して、実際の `path` の値を代入してあげる必要があります。

ではコールバックの設定方法ですが、 `CATransaction` を使って実装します。
`CATransaction#setCompletionBlock` の中に処理を書きます。
`CATransaction` は `UIView#animateWithDuration` の `finished` のフラグが無いので、completionの中で、実行した `CATransaction` のインスタンスの有無を確認することで代替します。
参考：http://qiita.com/inamiy/items/bdc0eb403852178c4ea7

```
// アニメーション完了のコールバックを設定
CATransaction.setCompletionBlock({
    // 各Layerに設定された `CAAnimaton` のインスタンスを取得
    let tabAnimation = self.tabLayer?.animationForKey("tabAnimation")
    let labelAnimation = self.labelLayer?.animationForKey("labelAnimation")
    
    //  `CAAnimaton` のインスタンスの有無でアニメーションの完了を判定する（nilの場合は finished=true に相当する）
    if tabAnimation != nil {
        // tabLayerからアニメーションを取り除く
        self.tabLayer?.removeAnimationForKey("tabAnimation")
        // pathをアニメーション後の値にセットする
        self.tabLayer?.path = self.tabLayerPath()
    }
    if labelAnimation != nil {
        // labelLayerについてもtabLayerと同様にコールバック処理を行う
        self.labelLayer?.removeAnimationForKey("labelAnimation")
        self.labelLayer?.path = self.labelLayerPath()
    }
})
```

これでTipsは終わりです。
特に難しいわけでもないので書くことも少なかったですが、せっかく作ったのでQiitaに書いてみました。
この記事に載せたサンプルコードはほんの一部なので、[実際のコード](https://github.com/yoshimin/SlitTabViewSample)の方もよければ動かしてみてください。