やること
SwiftUIとCombineを用いて無限スクロールを実装します。
このような仕様のアプリを作ってみました。
- TextFieldで何かしら検索を行なって、結果をリストに格納し、表示する
- スクロールが終わりそうなときに追加のコンテンツを読み込み、リストに追加し、表示する
2の部分について書いていきます。
実際にはAPIを呼ぶことを想定していますが
今回は無限スクロールをどうやって実現するのかという点に絞ります。
環境
macOS 11.4
xcode13.1
swift5
できたもの
無限スクロールの記事を書きました。
— Yusuke Miyata (@yuskey38) December 8, 2021
スクロールバーをよくみないとわからないかも。。。
【SwiftUI × Combine】さくっと無限スクロールを実装する #Qiitahttps://t.co/SsedGsNrcZ pic.twitter.com/feDFyHzl9S
実装
作成するファイルはこの3つです。
- SearchView.swift
- SearchViewCell.swift
- SearchViewModel.swift
大まかな処理はこんな感じです
- 取得したコンテンツのデータObjectのIDをIntにする。
- LazyVGridの各Itemが生成されるタイミングでそのItemのIDをViewModelに送る
- 受け取ったIDが最後からn番目のIDであれば読み込みを行う
1. 取得したコンテンツのObjectのIDをIntにする
struct Symbol: Identifiable {
var id = 0
var image: String
}
2. LazyVGridの各Itemが生成されるタイミングでIDをViewModelに送る
// SearchView.swift
VStack {
TextField("Search", text: $viewModel.searchText)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.padding(.horizontal)
ScrollView {
LazyVGrid(columns: Array(repeating: .init(.flexible(minimum: 0)), count: 2), spacing: 8) {
ForEach(viewModel.cellDataList) { symbol in
// ここが重要
SearchViewCell(symbol: symbol)
.onAppear { viewModel.loadNext(symbol.id) }
}
}
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification)) { _ in
viewModel.search.send()
}
スクロールが終わりそうなことの検知するために
各CellにPassthroughSubject
を渡して, Itemが生成されたタイミングで、IDをViewModelに渡しています。
SearchViewCellが表示されたタイミングでPassthroughSubject
でIDをViewModelに渡しています
3. 受け取ったIDが最後からn番目のIDであれば読み込みを行う
// SearchViewModel.swift
// これが重要
private var loadingId: Int {
perPage * page - 10
}
init() {
loadNext
.sink(receiveValue: { [weak self] id in
guard let self = self,
self.hasNextPage,
self.loadingId == id,
!self.loadedIdList.contains(id) else {
return
}
self.loadedIdList.append(id)
self.page += 1
self.cellDataList = self.cellDataList + self.makeSymbols(page: self.page)
if self.page == 5 { self.hasNextPage = false }
})
.store(in: &cancellables)
}
最後から10番目のIDのときに追加のコンテンツを読み込むため
loadingIdを用意しました。
そして下記3つがすべてtrueになる場合にのみ、追加のコンテンツを読み込むようにすることで無駄なリクエストが発生しないようになっています。
- 読み込むコンテンツが存在する
-
loadNext
で渡されたIDが最後から10番目のID - 一度読み込みを行なったIDではない
最後に
100件読み込んだら最初のn件を削除して再度読み込むようにするなど
キャッシュしている件数は固定にしておかないとメモリをどんどん消費することになりそうなので
次回はその辺も考慮した記事を書きたいと思います。
何か懸念点や問題点等あればぜひコメントお願いします!