1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI × Combine】さくっと無限スクロールを実装する

Last updated at Posted at 2021-12-08

やること

SwiftUIとCombineを用いて無限スクロールを実装します。

このような仕様のアプリを作ってみました。

  1. TextFieldで何かしら検索を行なって、結果をリストに格納し、表示する
  2. スクロールが終わりそうなときに追加のコンテンツを読み込み、リストに追加し、表示する

2の部分について書いていきます。
実際にはAPIを呼ぶことを想定していますが
今回は無限スクロールをどうやって実現するのかという点に絞ります。

環境

macOS 11.4
xcode13.1
swift5

できたもの

実装

作成するファイルはこの3つです。

  • SearchView.swift
  • SearchViewCell.swift
  • SearchViewModel.swift

大まかな処理はこんな感じです

  1. 取得したコンテンツのデータObjectのIDをIntにする。
  2. LazyVGridの各Itemが生成されるタイミングでそのItemのIDをViewModelに送る
  3. 受け取った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件を削除して再度読み込むようにするなど
キャッシュしている件数は固定にしておかないとメモリをどんどん消費することになりそうなので
次回はその辺も考慮した記事を書きたいと思います。

何か懸念点や問題点等あればぜひコメントお願いします!

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?