【Swift】UITableViewを使って無限スクロールする方法

2019年12月12日

この記事ではUITableViewを使って無限スクロールする方法を書いていきます。


以下、完成イメージです。

無限スクロールを実装するにあたって方法はいくつかあると思いますが、調べてた時によく見かけたのが以下の2つの方法です。

  1. scrollViewDidScroll  / scrollViewDidEndDraggingのdelegate methodを使った方法
  2. tableView will Display cellのdelegate methodを使った方法
結論から先に言うと、私は②のtableView will Display cellのdelegate methodを使った方法を推奨します。
※理由は後述します。
この記事では両方のアプローチを書いていきますが、推奨する方法にしか興味無い方は「3.tableView will Display cellを使った方法」までスキップしてください。

1.準備

今回は例として、メディアアプリのように記事一覧を表示する形にしていきます。

1-1. Viewの準備

まずはViewの準備からやっていきます。ViewControllerの中にUITableViewが置いてあるだけのシンプルな構成です。

ViewControllerにはUITableViewのoutletを追加します。

次に、tableViewのcellを作ります。

cellの中には記事のタイトルを表示するためのUILabelをcellの中央に配置しています。

TableCell classにはUILabelのoutletを追加します。

1-2. ViewModelの準備

無限スクロールするにはどこかのデータベースから少しずつ情報を取得する必要があります。今回はCloud Firestoreを使います。

この記事ではFirestoreの設定については触れませんが、気になる方は公式ページがご覧ください。

以下2つのクラスを追加します。

  1. Article.swift (Model)
  2. ArticleCollection.swift (ViewModel)

ArticleCollectionの中には2つのメソッドがあります。それぞれ名前の通りですが、fetchArticlesメソッドが最初だけ呼ばれるメソッドで、決めた数だけのドキュメントを取得します。それ以降はfetchMoreArticlesメソッドから決まった数の記事を取得していきます。

Firestoreには便利なクエリが用意されていて、「start(afterDocument: DocumentSnapshot)」を使えば、同じクエリ内容で開始地点を変えることが出来ます。

それでは準備が出来たので、2通りの方法を見ていきます。

2.scrollViewDidScrollを使った方法

scrollViewのdelegate methodを使った方法を見ていきます。UITableViewのdelegateをViewControllerに渡していれば、scrollViewのdelegate methodも使えるので、ViewControllerに以下を追加します。

ただscrollViewDidScrollですと、スクロールする度に記事を追加で取得するかの判定を行うので、処理が重くなりがちです。さらには、判定領域に入った後もスクロールし続けるので繰り返し記事を取得してしまい、一覧の中に重複する値が入ることがあります。

以下が実際のgif です。

scrollViewDidScrollですと、スクロールする度に呼び出されてしまうので、scrollViewDidEndDraggingを使ったほうが良いと言う人もいます。scrollViewDidEndDraggingはユーザーがスクロール後に指を画面から離したら呼び出されるメソッドです。

確かにscrollViewDidScrollと比較するとscrollViewDidEndDraggingの方が安定した挙動になりますが、touch upに反応するので指を離さなかったら追加で記事を取得しないのと、下までスクロールする度にカクつくので、ユーザーに取ってスムーズな体験にはならないかもしれません。

以下がscrollViewDidEndDragging版のgif です。

どうしてもscrollViewのdelegate methodで無限スクロールを実装したい場合scrollViewDidEndDraggingを使った方がマシだと思いますが、以下3つの理由から推奨はしません。

スクロール判定がダメだと思う理由:

  1. スクロールする度に判定メソッドが呼ばれ、判定領域に入ったらtableをreload。これが複数回呼ばれる可能性があるため、重複することがある上にtableViewがカクつく
  2. 無駄に判定メソッドが何回も呼ばれることが非効率的だと思う。
  3. 上にスクロールした際にも呼ばれる可能性がある。(判定領域内に入っていれば良いので、方向までは判断軸に含まれていない)

3.tableView will Display cellを使った方法

次に、tableView will Display cellのdelegate methodを使った方法を書いていきます。

ViewControllerを以下のように変更します。

willDisplay cellは〇〇番目のcellが表示される時に呼びだされるメソッドです。この記事では、まず記事を格納してる配列の総数が20個以上あり、後ろから10番目の記事が表示される時に追加で記事を取得するようになっています。

以下が実際のgif です。

このパターンですと、indexPath.rowが重複することがないので、無駄にfetchMoreArticlesが走る恐れもありませんし、上にスクロールしたとしてもcellを表示したあとにwillDisplay cellが呼ばれることもないので意図しないタイミングでfetchMoreArticlesが呼ばれる心配もありません。

ですので、UITableViewを使って無限スクロールを実装する場合はtableViewのwillDisplay cellの delegate methodをオススメします。

以上、UITableViewを使って無限スクロールする方法でした。

おまけ:CloudFirestoreクエリ:offset vs startAfter

CloudFirestoreでページネーションを行う場合、以下のクエリ方法があります。

  1. offset()
  2. startAt() / startAfter()

結論から言いますと、何か理由がない限りはstartAt() / startAfter()を使いましょう。

なぜかと言うと、offsetメソッドはスキップしたドキュメントまでも読み込むらしく、課金されるので注意が必要だからです。

以下のようなクエリがあったとします:

db.collection(‘restaurants’)
.where( ‘city’ == ‘Tokyo’ )
.limit(to: 20)
.offset( 40 )

↑は60ドキュメント読み込まれ、課金されます。

startAt / startAfterメソッド

startAt / startAfterメソッドは実際の開始地点からドキュメントが読み込まれるので飛ばしたドキュメントまでは読み込まれません。(この記事でもstartAfterを使っています)

参考URL