UIView.animateくらい簡単にカスタムイージング

UIKitを使ったアニメーションでカスタマイズしたイージングを使う方法がわかったので共有です。色々調べた結果、UIView.animateくらい簡単にカスタムイージングを設定することができました。

<details>
<summary>イージングとは...</summary>
アニメーションの緩急のこと。これを調整することで、ぬるっとしたアニメーションなどを実現させることができます。
[世界一わかりやすい「イージング」と、その応用｜ritar｜note](https://note.com/ritar/n/n5e8ed0e07917)とかがわかりやすいと思います。

イージングの調整は、３次ベジェ曲線でパラメータを指定します。（他の指定方法を知らないだけかもしれません...）
３次ベジェ曲線とイージングの関係や調整は[cubic-bezier.com](https://cubic-bezier.com/#.17,.67,.83,.67)で色々試してみると理解しやすいと思います。
</details>

### UIView.animateでのイージング

iOSでのアニメーションといえば[UIView.animate](https://developer.apple.com/documentation/uikit/uiview/1622515-animate)がとてもお手軽です。イージングは `options:` 引数で指定できるのですが、規定のもの（`.curveLinear`, `.curveEaseIn`, `.curveEaseOut`, `.curveEaseInOut`）しか使うことができません。

```swift
UIView.animate(withDuration: 1.0, delay: 0, options: .curveLinear) {
    linearConstraint.constant = 288
    liveView.layoutIfNeeded()
}
```

カスタマイズしたイージングを使おうと思うと、CoreAnimationの[CAMediaTimingFunction](https://developer.apple.com/documentation/quartzcore/camediatimingfunction)などを使うことになるのかなと思っていたのですが、[UIViewPropertyAnimator](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator)を使えば、UIKitの世界だけでとても簡単に実現できました。

### UIViewPropertyAnimatorでのイージング

```swift
UIViewPropertyAnimator(duration: 1.0,
                       controlPoint1: .init(x: 0.2, y: 1),
                       controlPoint2: .init(x: 1, y: 0.2)) {
    customEasingConstraint.constant = 288
    liveView.layoutIfNeeded()
}.startAnimation()
```

UIView.animateとそれほど変わらないコード量で実装できました 🎉
コードを解説しておくと以下の通りとなります。

|UIViewPropertyAnimator()<br>の引数|説明|
|:---:|:---:|
|`duration:`|アニメーション秒数。<br>`UIView.animate`の`withDuration:`と同じ。|
|`controlPoint1`,<br>`controlPoint2`|イージングの設定。３次ベジェ曲線の制御点を指定する。<br><br>上の例では途中で止まるような極端な例にしています。<br>c.f. https://cubic-bezier.com/#0,1,1,0|
|`animations:`<br>（上の例ではtrailingClosure）|`UIView.animate`の`animations:`と同じ。|

UIViewPropertyAnimatorをインスタンス化して、 `startAnimation()` でアニメーションを開始できます。

#### Demo

* 青：UIView.animateのoption指定なし（デフォルトで `.curveEaseInOut` が適用される）
* 赤：UIView.animateのoption: `.curveLinear`
* 緑：UIViewPropertyAnimator

![easing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36771/9eee7571-4065-c2a2-7f85-58a93f89263c.gif)

アニメーションGIFなのでわかりにくいかもですが、緑のバーは途中で止まって再開するようなイージングになっています。

#### Demoコード

Demoに使ったコードの全体です。Playgroundで実行できる形になってますので、ぜひ動かしてみてください。

```swift
import UIKit
import PlaygroundSupport

// レイアウト準備
let frame = CGRect(x: 0, y: 0, width: 320, height: 320)
let liveView = UIView(frame: frame)
liveView.backgroundColor = .white
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .leading
stackView.spacing = 8
liveView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    stackView.leadingAnchor.constraint(equalTo: liveView.leadingAnchor,
                                       constant: 16),
    stackView.centerYAnchor.constraint(equalTo: liveView.centerYAnchor)
])
let animateView = UIView()
animateView.backgroundColor = .blue
let linearView = UIView()
linearView.backgroundColor = .red
let customEasingView = UIView()
customEasingView.backgroundColor = .green
[animateView, linearView, customEasingView].forEach {
    stackView.addArrangedSubview($0)
}
let animateConstraint = animateView.widthAnchor.constraint(equalToConstant: 0)
let linearConstraint = linearView.widthAnchor.constraint(equalToConstant: 0)
let customEasingConstraint = customEasingView.widthAnchor.constraint(equalToConstant: 0)
NSLayoutConstraint.activate([
    animateConstraint,
    animateView.heightAnchor.constraint(equalToConstant: 32),
    linearConstraint,
    linearView.heightAnchor.constraint(equalToConstant: 32),
    customEasingConstraint,
    customEasingView.heightAnchor.constraint(equalToConstant: 32)
])
liveView.layoutIfNeeded()

// アニメーション
UIView.animate(withDuration: 1.0) {
    animateConstraint.constant = 288
    liveView.layoutIfNeeded()
}
UIView.animate(withDuration: 1.0, delay: 0, options: .curveLinear) {
    linearConstraint.constant = 288
    liveView.layoutIfNeeded()
}
UIViewPropertyAnimator(duration: 1.0,
                       controlPoint1: .init(x: 0.2, y: 1),
                       controlPoint2: .init(x: 1, y: 0.2)) {
    customEasingConstraint.constant = 288
    liveView.layoutIfNeeded()
}.startAnimation()

// Playground準備
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = liveView
```

#### completionもつけれる

UIView.animateにも用意されているcompletionもつけることができます。その場合は一度変数で受ける必要があります。

```swift
let animator = UIViewPropertyAnimator(duration: 1.0,
                       controlPoint1: .init(x: 0.2, y: 1),
                       controlPoint2: .init(x: 1, y: 0.2)) {
    customEasingConstraint.constant = 288
    liveView.layoutIfNeeded()
}
animator.addCompletion { _ in
    customEasingView.backgroundColor = .black
}
animator.startAnimation()
```

その他にも、一時停止したり、逆再生したり、UIView.animateより複雑なことができるようです（試してない）
もっと使いこなしたい方は [UIViewPropertyAnimatorを使いこなそう - Qiita](https://qiita.com/hachinobu/items/bc2e1fa0c3bbd0a51d17) こちらの記事がとても参考になると思います。

### まとめ

これで気軽にイージングが試せますね💪
どなたかの助けになれば幸いです🙏

### 参考

* [UIViewPropertyAnimatorを使いこなそう - Qiita](https://qiita.com/hachinobu/items/bc2e1fa0c3bbd0a51d17)
* [UIViewPropertyAnimatorの基本まとめ① - Qiita](https://qiita.com/TsukasaHasegawa/items/4b112babb48c7a9b6de4)
* [[iOS] UIViewPropertyAnimatorに良い感じのイージングを使いたい！ | DevelopersIO](https://dev.classmethod.jp/articles/uiviewpropertyanimator-custom-easing-equations/)
* [UIView.animateにおけるEasingへの疑いを晴らす - そして信用へ - - Qiita](https://qiita.com/haguhoms/items/aba5676c93404680c102)