LoginSignup
2
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-11

はじめに

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

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

目次

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

環境

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

実行例

refreshanime.gif

考え方

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

1.画面構成

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

Untitled Diagram.png

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

このUIImageViewGIFアニメーションを表示する領域となります。

2.ロード時の画面表示

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

GIFアニメーションを表示する

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

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

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

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

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

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

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)
    }

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


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

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は以下のよう表現されます。

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()しているところですね。


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

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


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

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

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

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

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

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

 refreshCtl.tintColor = .clear

UIImageViewをロード時に固定表示し、処理後に初期状態に戻す

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


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

 myTableView.contentInset.top = 150

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

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

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


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

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

    @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にあるようなスクロールによって閉じたり開いたりするヘッダー

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3