[Swift5]ビリビリ動画にあるようなリフレッシュする際にGIFアニメーションを見せるビュー

# はじめに
TableViewを引っ張ってリロードすることを**リフレッシュ**と呼びますが、
リフレッシュする際にアニメーションを見せることにより、**ユーザが待ち時間に感じる退屈**を防ぐことができます。
実際に中国大手の動画共有サイト「bilibli」のスマホアプリでも取り入れられています。
(筆者はbilibiliの**世界観を創り出すUI**をとても気に入っており、UIに困ると再現して取り込んでしまっています。)

この記事ではリフレッシュする際にGIFアニメーションを見せるTableViewの作り方を共有したいと思います。

# 目次
- 環境
- 実行例
- 考え方
    - 画面の構成
    - ロード時に画面を固定して見せる 
        - GIFアニメーションを表示する
        - UIImageViewをロード時に固定表示し、処理後に初期状態に戻す
- ソースコード
- おわりに

# 環境
- Xcode 11.2.1
- Swift5
- [SwiftGifOrigin 1.7.0](https://github.com/swiftgif/SwiftGif)

SwiftGifOriginはCocoaPodsから簡単に入れられます。

# 実行例
![refreshanime.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576493/96d4bbbb-72a5-eba6-e249-99057a3a103e.gif)

# 考え方
とてもシンプルに実装することができます。
考えることは、**画面の構成**と**ロード時の画面表示**についてだけです。
(と言っても細かい工夫は必要になってきます。)

## 1.画面構成
画面構成(初期状態)は以下のようになっています。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576493/d599f5f5-dcfd-925f-788d-7f5d7ff6612a.png)

`TableView`があって、その上に`UIView`が被さっていることがわかります。
さらに`UIView`には`header`と`UIImageView`が被さっていることがわかりますね。

この`UIImageView`が**GIFアニメーションを表示する領域**となります。

## 2.ロード時の画面表示
ロード時の画面表示でやるべきことは主に2つあります。
**GIFアニメーションを表示すること**と、
**UIImageViewをロード時に固定表示し、処理後に初期状態へ戻すこと**です。

### GIFアニメーションを表示する
GIFアニメーションの表示には**SwiftGifOrigin**という便利なライブラリを利用します。

また、GIFアニメーションはロード時に動かすことが望ましいですよね。
なのでViewの生成時には画像を表示し、ロード時になった場合にGIFを表示するようなライフサイクルにしていこうと思います。
(このテクニックはスプラッシュ画面の作成時などにも利用されます)

以上のことを実現するために必要な関数が３つあります。

|実行順番|関数名|処理タイミング|処理内容|
|:-:|:--|:--|:--|
|1|`createHeaderView`|viewDidLoad時|ビューを作成する|
|2|`addHeaderViewGif`|リフレッシュ時の最初|ビューにGIFを追加する|
|3|`updateHeaderView`|リフレッシュ時の最後|ビューをアップデートする|

---
では、それぞれの関数について説明していきます。

`createHeaderView`は以下のように表現されます。

~~~Swift
private func createHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
        
        myLabel.text = "↑"
        myLabel.textAlignment = .center
        myLabel.textColor = .white
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
       
        image.image = UIImage(named: "bili")
        
        myHeaderView.addSubview(image)
    }
~~~

ヘッダービューを作成して、そこに`UILabel`と`UIImageView`を貼り付けているだけですね。

---

`addHeaderViewGif`は以下のように表現されます。

~~~Swift
func addHeaderViewGif() {
        let displayWidth: CGFloat! = self.view.frame.width
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        
        image.loadGif(name: "bili")
       
        myHeaderView.addSubview(image)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.myHeaderView.subviews[1].removeFromSuperview()
        }
    }
~~~

ここでGIFアニメーションを追加していますね。

処理の内容としては、

`image.loadGif(name: "bilibili")`でGIFを`UIImageView`にセットして、
静止画像の上に重ねるようにヘッダビューに貼り付けています。

そして、その0.1秒後に静止画像がセットされている`UIImageView`を削除しています。

ここで**重要なのは処理の順番**です。

もしこの処理の流れを逆にしてしまえば、`UIImageView`が一瞬存在しない時間が生まれてしまいます。
そのせいで、一瞬消えて再表示されるように見えてしまうんですね。
これでは切り替えているのがバレバレで、自然にアニメーションが動くように感じられません。

それを避けるために、**静止画とGIFアニメーションが0.1秒重なるよう**に表示します。

---

`updateHeaderView`は以下のよう表現されます。

~~~Swift
private func updateHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        for sub in myHeaderView.subviews {
            sub.removeFromSuperview()
        }
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
        
        myLabel.text = "↑"
        
        myLabel.textAlignment = .center
        myLabel.textColor = .white
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
      
        image.image = UIImage(named: "bili")
        
        myHeaderView.addSubview(image)
    }
~~~

`createHeaderView`とほとんど同じ処理です。

違うのはビューを重ねないように全て削除してから`addSubView()`しているところですね。

---

以上で関数の説明は終わりです。

リフレッシュに関数する説明も少ししておきます。

---

リフレッシュ時に呼び出される関数は以下のように定義されます。

~~~Swift
  @objc func refresh(sender: UIRefreshControl) {...}
~~~

また、リフレッシュをどのように実装するかについては解説記事がたくさん出ているので、そちらを参考にしてもらえれば良いと思います。(簡単にできます)

ただ、**リフレッシュに関して1つだけ工夫**があるのでそこだけ説明したいと思います。

通常、リフレッシュではインジケータ(くるくる回るやつ)が表示されてしまいます。
しかし**アニメーションを見せるのには邪魔**ですよね。
なので、見えないようにします。

具体的には以下のようにしてインジケータを透明に設定します。

~~~Swift
 refreshCtl.tintColor = .clear
~~~

---


### UIImageViewをロード時に固定表示し、処理後に初期状態に戻す
ロード時はGIFアニメーションが見えるように、UITableViewを引っ張ったままの状態で表示した方が良いですよね。
もちろんロード処理が終わった後は元に戻してあげないといけません。

---

`UIImageView`を見えるように固定する方法は以下のように表現されます。

~~~Swift
 myTableView.contentInset.top = 150
~~~

これは`TableView`の上に150分の余白を与えるという意味です。
150というのはヘッダービューにおける**`Header`と`UIImageView`の高さの和**ですね。

**ヘッダービューの高さそのものではないことに注意**してください。
ヘッダービューの高さに余裕を持たせておいてある可能性もありますから。
(ヘッダービューと背景の色を異なるものにしている場合、スクロールした際にUIImageのすぐ上に背景が見えるのを防ぐため)

こうすることで`TableView`の上にくっつくようにして配置されているヘッダービューのヘッダーとGIFアニメーションが見えるようになります。

---

ロード処理が完了した後に、徐々にヘッダービューを閉じていく方法は以下のように表現されます。

~~~Swift
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
            UIView.animate(withDuration: 0.5, delay: 0.0, options: [],animations: {
                self.myTableView.contentInset.top = 30
            }, completion: nil)
        }
~~~

これは、2.5秒後に0.5秒かけて`TableView`の余白を30にするという意味です。

つまり`DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {...}`を利用して**2.5秒間GIFアニメーションを表示**し、
`UIView.animate`でヘッダビューの位置を**アニメーションして戻していく**ようにしているということですね。

# ソースコード

主要なソースコードは以下に載せておきます。

Githubにサンプルをあげておくので参考にしてみてください。
このサンプルは実行例にあるものとは違います。(わかりやすくするため。要望があれば実行例の説明もしたいと思います。)
https://github.com/Hajime-Ito/SampleRefreshAnimation

~~~Swift
    @objc func refresh(sender: UIRefreshControl) {
        self.addHeaderViewGif()
        myTableView.contentInset.top = 130
        sender.endRefreshing()
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            UIView.animate(withDuration: 0.5, delay: 0.0, options: [],animations: {
                self.myTableView.contentInset.top = 30
            }, completion: nil)
            self.updateHeaderView()
        }
    }

    private func createHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = .white
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
        myLabel.text = "header"
        myLabel.textAlignment = .center
        myLabel.textColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.image = UIImage(named: "bili")
        myHeaderView.addSubview(image)
    }
    
    private func updateHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView.subviews[1].removeFromSuperview()
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.image = UIImage(named: "bili")
        myHeaderView.addSubview(image)
    }
    
    private func addHeaderViewGif() {
        let displayWidth: CGFloat! = self.view.frame.width
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.loadGif(name: "bili")
        myHeaderView.addSubview(image)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.myHeaderView.subviews[1].removeFromSuperview()
        }
    }
~~~

# 終わりに
ここまで読んでくれたみなさんお疲れ様でした。
ちょっとした工夫がたくさん必要になるような実装でしたね。

しかし、良いと思うものを再現するのは面白いものです。。

実はサンプルプログラムではこの記事では説明しきれなかったプログラムを使用しています。
役に立つと思うので、そちらもチェックしてみてください。

[[Swift5]ニコニコ動画やLINEにあるようなスクロールによって閉じたり開いたりするヘッダー](https://qiita.com/Hajime-Ito/items/2ef143b02ba65d43001d)