はじめに
APIで取得したデータをListで表示して、末尾までスクロールすると再取得する実装を考えてみました。
内容
このようなデータを取得してList表示する場合を考えてみようと思います
struct Fruits: Identifiable, Hashable {
let id: Int
let name: String
let price: Int
}
APIからのデータ取得ではなく、擬似的なデータ追加取得をさせて動きを確認します
ここではAPIから5件づつデータを取得できるとし、取得データが5件未満であれば、全取得済みという形となります
class SampleViewModel: ObservableObject {
@Published var isLoading: Bool = false
@Published var canLoadMore: Bool = false
@Published var fruitsList: [Fruits] = []
@MainActor private func toggleIsLoading(isLoading: Bool) {
self.isLoading = isLoading
}
@MainActor private func setUpData(_ data: [Fruits]) {
self.fruitsList = data
self.canLoadMore = data.count == 5
}
@MainActor private func addData(_ data: [Fruits]) {
self.fruitsList.append(contentsOf: data)
self.canLoadMore = data.count == 5
}
func fetch() async {
await toggleIsLoading(isLoading: true)
sleep(1)
let fetchedData = [
Fruits(id: 1, name: "りんご", price: 200),
Fruits(id: 2, name: "みかん", price: 200),
Fruits(id: 3, name: "ぶどう", price: 300),
Fruits(id: 4, name: "いちご", price: 400),
Fruits(id: 5, name: "なし", price: 400)
]
await setUpData(fetchedData)
await toggleIsLoading(isLoading: false)
}
func fetchMore() async {
guard !isLoading, fruitsList.count % 5 == 0 else { return }
await toggleIsLoading(isLoading: true)
sleep(1)
let nextPage = fruitsList.count / 5 + 1
switch nextPage {
case 2:
let fetchedData = [
Fruits(id: 6, name: "スイカ", price: 800),
Fruits(id: 7, name: "洋なし", price: 200),
Fruits(id: 8, name: "ブルーベリー", price: 400),
Fruits(id: 9, name: "メロン", price: 800),
Fruits(id: 10, name: "びわ", price: 200)
]
await addData(fetchedData)
case 3:
let fetchedData = [
Fruits(id: 11, name: "パイナップル", price: 500),
Fruits(id: 12, name: "オレンジ", price: 200),
Fruits(id: 13, name: "バナナ", price: 200),
Fruits(id: 14, name: "キウイ", price: 200),
Fruits(id: 15, name: "レモン", price: 200)
]
await addData(fetchedData)
default:
let fetchedData = [
Fruits(id: 16, name: "もも", price: 400),
Fruits(id: 17, name: "マスカット", price: 900)
]
await addData(fetchedData)
}
await toggleIsLoading(isLoading: false)
}
}
追加ローディングViewをリストの末尾に配置する
リストの最後に追加取得Viewを配置して、そのViewの.onAppear
が呼ばれたタイミングで追加取得の処理をさせるようにしました
struct ContentView: View {
@StateObject private var viewModel: SampleViewModel = SampleViewModel()
var body: some View {
ZStack {
List {
ForEach(viewModel.fruitsList) { item in
VStack(alignment: .leading) {
Text("ID") + Text(item.id.description)
Text(item.name)
Text(item.price.description) + Text("円")
}
.font(.system(size: 16, weight: .bold))
.frame(maxWidth: .infinity, minHeight: 150, alignment: .topLeading)
}
if viewModel.canLoadMore {
Text("追加読み込み")
.onAppear {
Task {
await viewModel.fetchMore()
}
}
}
}
.task {
await viewModel.fetch()
}
if viewModel.isLoading {
ProgressView()
}
}
}
}
ちなみに.onAppear
の部分を.task
にしてみると
if viewModel.canLoadMore {
Text("追加読み込み")
.task {
await viewModel.fetchMore()
}
}
以前.onAppear
と.task
についてまとめましたことがありましたが、.task
に書き換えても今回の実装では挙動が変わらずでした
Listのセルに .onAppear
を追加してリストの最後かどうか判定する
ObservableObject
の追加取得ロジックを少し修正して、fetchMoreにitemを渡し、そのitemがリストの最後なら追加取得させるようにする
func fetchMore(_ item: Fruits) async {
guard item.id == fruitsList.last?.id else { return }
guard !isLoading, fruitsList.count % 5 == 0 else { return }
await toggleIsLoading(isLoading: true)
そしてList
のセルに.onAppear
を追加することで末尾までスクロールすると追加取得できるようになる
struct ContentView: View {
@StateObject private var viewModel: SampleViewModel = SampleViewModel()
var body: some View {
ZStack {
List {
ForEach(viewModel.fruitsList) { item in
VStack(alignment: .leading) {
Text("ID") + Text(item.id.description)
Text(item.name)
Text(item.price.description) + Text("円")
}
.font(.system(size: 16, weight: .bold))
.frame(maxWidth: .infinity, minHeight: 200, alignment: .topLeading)
.onAppear {
Task {
await viewModel.fetchMore(item)
}
}
}
}
.task {
await viewModel.fetch()
}
if viewModel.isLoading {
ProgressView()
}
}
}
}
リストの最後かどうかを判定するロジックを追加してみる
こちらの記事を参考にすると
extension RandomAccessCollection where Self.Element: Identifiable {
public func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
}
こんな形にもできるようです
func fetchMore(_ item: Fruits) async {
guard fruitsList.isLastItem(item) else { return }
guard !isLoading, fruitsList.count % 5 == 0 else { return }
await toggleIsLoading(isLoading: true)
さいごに
たくさん調べましたが、やはり.onAppear
を使う方法が一般的なのかなと思いました。
他にもあればぜひ教えて頂きたいところです。
参考