CAShapeLayer による図形のストロークアニメーション

iOS で図形を描画したい場合、`CAShapeLayer` を使うと、様々な図形を描画することができます。
また、Core Animation を使うことで、図形をアニメーションさせることができます。

簡単な実例を Xcode の Playground で試してみます。

# Playground の準備

まず、Xcode で iOS 用の Playground を作成して、View を表示します。

```swift
import UIKit
import PlaygroundSupport

let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: 400, height: 400)
view.backgroundColor = .white
PlaygroundPage.current.liveView = view
```

View のサイズはなんでも良いですが、とりあえず 400x400 にしておきました。次のように白い View が表示されます。

<img width="1281" alt="Playground.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6204/904a473b-4a0e-f0dc-772e-449ed77146e4.png">

# CAShapeLayer の作成

先ほどの View に `CAShapeLayer` を追加します。

```swift
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: 400, height: 400)
shapeLayer.lineWidth = 20.0
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.cyan.cgColor
view.layer.addSublayer(shapeLayer)
```

まだ図形を設定していませんが、ひとまず線の幅（`lineWidth`）、線の色（`strokeColor`）、塗りつぶしの色（`fillColor`）を設定しておきます。

ここでは、単に何も表示しない `shapeLayer` を追加しただけなので、表示は変わりません。

# 弧の描画

弧を描画してみます。

```swift
let center = CGPoint(x: 200, y: 200)
let radius = CGFloat(100)
let startAngle = CGFloat(0.125 * .pi)
let endAngle = CGFloat(1.875 * .pi)

let arc = UIBezierPath(arcCenter: center,
                       radius: radius,
                       startAngle: startAngle,
                       endAngle: endAngle,
                       clockwise: true)
shapeLayer.path = arc.cgPath
```

`UIBezierPath` で、中心座標、半径、開始角、終了角を指定すれば、弧が作成できます。

角度の数値については [リファレンス](https://developer.apple.com/documentation/uikit/uibezierpath/1624358-init) に記載されています。3時の方向を0として、時計周りにラジアンで指定します（数学では反時計回りが一般的ですが、ここでは逆向きです）。ここでは、1/8 π ラジアンから、(2 - 1/8) π ラジアンまでの円弧を指定しました。

<img width="401" alt="Arc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6204/411956f2-8cbc-e6a8-e842-be2e3be2301d.png">

# 弧のストロークアニメーション

弧の描画をアニメーションで表示します。

```swift
let animation = CAKeyframeAnimation(keyPath: "strokeEnd")
animation.values = [0.0, 1.0, 1.0]
animation.keyTimes = [0.0, 0.3, 1.0]
animation.duration = 3.0
animation.repeatCount = .greatestFiniteMagnitude
shapeLayer.add(animation, forKey: nil)
```

`CAShapeLayer` は様々なアニメーションが可能ですが、今回は [`strokeEnd`](https://developer.apple.com/documentation/quartzcore/cashapelayer/1522252-strokeend) を使います。

`strokeStart` と `strokeEnd` はペアで使うプロパティで、`CAShapeLayer` の `path` のうち、実際に描画する範囲を `0.0` から `1.0` の間で指定します。デフォルトでは `strokeStart` が `0.0` で `strokeEnd` が `1.0`、つまり `path` の全体を描画します。仮に `strokeEnd` を `0.5` にすると、`path` の最初から半分の位置までが描画されます。

`strokeEnd` は [リファレンス](https://developer.apple.com/documentation/quartzcore/cashapelayer/1522252-strokeend) にも書いてあるように Animatable なプロパティであり、値の変化をアニメーションで表示できます。そこで、`strokeEnd` を `0.0` から `1.0` にアニメーションで変化させることで、線が徐々に描画されるアニメーションになります。

また、アニメーションには `CABasicAnimation` がよく使われますが、今回は `CAKeyframeAnimation` を使いました。

`values` と `keyTimes` を使って値の変化を指定します。上記のコードは、時間 `0.0` から `0.3` の間に値を `0.0` から `1.0` に変化させ、時間 `0.3` から `1.0` の間では値は `1.0` から `1.0` のままにする、という指定をしています。アニメーション全体の時間 `duration` は 3 秒に指定しているため、時間 `0.3` は 3 x 0.3 = 0.9 秒を意味します。

アニメーションを繰り返す場合に、いったん静止状態を挟むのは `CAKeyframeAnimation` が簡単かと思います。特に繰り返しが必要なければ、`CABasicAnimation` を使うほうが素直で分かりやすいかと思います。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">弧のストロークアニメーション <a href="https://t.co/Of29omEqaE">pic.twitter.com/Of29omEqaE</a></p>&mdash; 宇佐見 公輔 (@usamik26) <a href="https://twitter.com/usamik26/status/1206029909681770498?ref_src=twsrc%5Etfw">December 15, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 扇形の描画

複数の線を連結させて図形を描画してみます。

```swift
let center = CGPoint(x: 200, y: 200)
let radius = CGFloat(100)
let startAngle = CGFloat(0.125 * .pi)
let endAngle = CGFloat(1.875 * .pi)

let fanShape = UIBezierPath()
fanShape.move(to: center)
fanShape.addLine(to: CGPoint(x: center.x + radius * cos(startAngle),
                             y: center.y + radius * sin(startAngle)))
fanShape.addArc(withCenter: center,
                radius: radius,
                startAngle: startAngle,
                endAngle: endAngle,
                clockwise: true)
fanShape.close()
shapeLayer.path = fanShape.cgPath
```

まず、円の中心にペンを置き（`move`）、円周に向かって線を引きます（`addLine`）。ある点から距離 `radius`、角度 θ 方向の点の座標は、X 座標に `radius * cos(θ)` を、Y 座標に `radius * sin(θ)` を足せば求められます。

次に、この線に続けて弧を書きます（`addArc`）。最後に `close` を呼ぶと、最初の点まで直線で繋がれて閉じた線ができあがります（なお、`close` しないと閉じた線という扱いにならないため、始点と終点が繋がって描画されずに不自然になります）。

<img width="400" alt="FanShape.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6204/6cebf3d5-bf49-ec74-2f80-6c4f84737053.png">

# 扇形のストロークアニメーション

扇形の描画をアニメーションで表示するコードは、弧のときとまったく同じです。

```swift
let animation = CAKeyframeAnimation(keyPath: "strokeEnd")
animation.values = [0.0, 1.0, 1.0]
animation.keyTimes = [0.0, 0.3, 1.0]
animation.duration = 3.0
animation.repeatCount = .greatestFiniteMagnitude
shapeLayer.add(animation, forKey: nil)
```

どんな図形でも `strokeEnd` を `0.0` から `1.0` に変化させれば良いので、難しいことを考える必要はありません。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">扇形のストロークアニメーション <a href="https://t.co/z4HFOG3YNe">pic.twitter.com/z4HFOG3YNe</a></p>&mdash; 宇佐見 公輔 (@usamik26) <a href="https://twitter.com/usamik26/status/1206030141442285568?ref_src=twsrc%5Etfw">December 15, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まとめ

Xcode Playground で今すぐ試せるアニメーションのコードを紹介しました。参考になれば幸いです。