はじめに
TableViewを引っ張ってリロードすることをリフレッシュと呼びますが、
リフレッシュする際にアニメーションを見せることにより、ユーザが待ち時間に感じる退屈を防ぐことができます。
実際に中国大手の動画共有サイト「bilibli」のスマホアプリでも取り入れられています。
(筆者はbilibiliの世界観を創り出すUIをとても気に入っており、UIに困ると再現して取り込んでしまっています。)
この記事ではリフレッシュする際にGIFアニメーションを見せるTableViewの作り方を共有したいと思います。
目次
- 環境
- 実行例
- 考え方
- 画面の構成
- ロード時に画面を固定して見せる
- GIFアニメーションを表示する
- UIImageViewをロード時に固定表示し、処理後に初期状態に戻す
- ソースコード
- おわりに
環境
- Xcode 11.2.1
- Swift5
- SwiftGifOrigin 1.7.0
SwiftGifOriginはCocoaPodsから簡単に入れられます。
実行例
考え方
とてもシンプルに実装することができます。
考えることは、画面の構成とロード時の画面表示についてだけです。
(と言っても細かい工夫は必要になってきます。)
1.画面構成
画面構成(初期状態)は以下のようになっています。
TableView
があって、その上にUIView
が被さっていることがわかります。
さらにUIView
にはheader
とUIImageView
が被さっていることがわかりますね。
このUIImageView
がGIFアニメーションを表示する領域となります。
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)
}
ヘッダービューを作成して、そこにUILabel
とUIImageView
を貼り付けているだけですね。
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というのはヘッダービューにおける**Header
とUIImageView
の高さの和**ですね。
ヘッダービューの高さそのものではないことに注意してください。
ヘッダービューの高さに余裕を持たせておいてある可能性もありますから。
(ヘッダービューと背景の色を異なるものにしている場合、スクロールした際に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()
}
}
終わりに
ここまで読んでくれたみなさんお疲れ様でした。
ちょっとした工夫がたくさん必要になるような実装でしたね。
しかし、良いと思うものを再現するのは面白いものです。。
実はサンプルプログラムではこの記事では説明しきれなかったプログラムを使用しています。
役に立つと思うので、そちらもチェックしてみてください。