ScrollView のスクロール中だけ Lottie アニメーションを止めたい！ (Swift)

## はじめに
Lottie アニメーションを `UIScrollView` がスクロールしている最中のみ動かしたいときがあったので、その実装を紹介します。
具体的に言うと、

- スクロール中: アニメーション停止
- スクロール終了: アニメーション再開

という要求です。

## 準備: Lottie を動かす ViewController
ScrollView の動作関係なしに、ただ Lottie を動かす ViewController のコードを紹介します。
今回はこのコードを元に実装してみます。

コメント多めに書いてますので、ある程度は追えるかと思います。
今回はなんとなくランダムな位置に複数の Lottie アニメーションを配置してみました。

```swift
import UIKit
import Lottie

class ViewController: UIViewController {

    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!

    // MARK: - Private properties
    var animations = [AnimationView]()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する
        // 複数配置していることに特に意味はありません。
        for _ in 1...3 {
            let x = CGFloat.random(in: 1.0...5.0) * 200.0
            let y = CGFloat.random(in: 1.0...6.0) * 100.0
            let animation = generateAnimation(x: x, y: y)
            animations.append(animation)
            contentView.addSubview(animation)
            animation.play()
        }
    }

    // MARK: - Private methods
    // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド
    // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4
    private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView {
        let animationView = AnimationView(name: "sample_animation")
        animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2)
        animationView.loopMode = .loop
        animationView.contentMode = .scaleAspectFit
        animationView.animationSpeed = 1

        return animationView
    }
}

```

## スクロール中だけアニメーションを止めてみよう
このままでは `viewDidLoad` ライフサイクルで `play()` されっぱなしで、止めようがありません。
今回の要求として、「スクロール中はアニメーションを止める」というのがありますので、まずは `UIScrollView` を Outlet 接続しましょう。

```swift
    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var backgroundScrollView: UIScrollView! // 追加
```

次に、追加した `backgroundScrollView` の delegate 先を `self` つまり `ViewController` 自身に設定します。

```swift
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        backgroundScrollView.delegate = self // 追加
```

これで ScrollView の delegate メソッドが使えるようになりました。
`UIScrollView` の delegate メソッドは沢山ありますが、今回は

- スクロールのし初め
- スクロールの終わり

を検知することで

- スクロールのし初め: アニメーションを一時停止
- スクロールの終わり: アニメーションを再開

という風にして実装していきます。
沢山ある delegate メソッドの解説は [公式ドキュメント](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate) や [他の方がまとめてくださった記事](https://program-life.com/724) に譲って割愛します。
今回使うのは、以下の 2 つです。

```swift
    // 1.
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView)

    // 2.
    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
```

これらのメソッドを使えば、以下のようにして、簡単にスクロール中にアニメーションを止めて、スクロールが終わったらアニメーションを再開するということが可能になります。
ちなみに、 `stop()` メソッドを使用していますが、 `pause()` メソッドでも大丈夫です。
アニメーションが最後まで行って止まるか、 `pause()` された瞬間に止まるかの違いです。

```swift
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }
```

あとは、これを実装して終わりです。
ViewController に `UIScrollViewDelegate` を継承させます。
僕は extension して書いていますが、 `class ViewController: UIViewController, UIScrollViewDelegate` としても何も問題ありません。

```swift
// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }
}
```

これでビルドしてみると、スクロールしている間だけアニメーションが止まることを確認できるはずです。
最後に、完成した ViewController を載せます。

```swift

import UIKit
import Lottie

class ViewController: UIViewController {

    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var backgroundScrollView: UIScrollView!

    // MARK: - Private properties
    var animations = [AnimationView]()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        backgroundScrollView.delegate = self

        // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する
        // 複数配置していることに特に意味はありません。
        for _ in 1...3 {
            let x = CGFloat.random(in: 1.0...5.0) * 200.0
            let y = CGFloat.random(in: 1.0...6.0) * 100.0
            let animation = generateAnimation(x: x, y: y)
            animations.append(animation)
            contentView.addSubview(animation)
            animation.play()
        }
    }

    // MARK: - Private methods
    // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド
    // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4
    private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView {
        let animationView = AnimationView(name: "sample_animation")
        animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2)
        animationView.loopMode = .loop
        animationView.contentMode = .scaleAspectFit
        animationView.animationSpeed = 1

        return animationView
    }
}

// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }
}

```

## 参考文献
- [Apple - UIScrollViewDelegate](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate)
- [【Swift4】UIScrollViewのデリゲート、スクロール開始・中・停止、ズーム開始・中・停止まとめ【iOS11】](https://program-life.com/724)
- [lottie-ios](https://github.com/airbnb/lottie-ios)