尽管我们可以访问List中的具体item,但是我们不知道List滚动到了当前哪个位置,也不知道我们到List末尾的距离。这些数据都是我们进行分页的基础。
Pagination(分页)对于每个人都有不同的含义,因此我们先给分页的目标做个明确定义:
在滚动过程中,List应提取并追加下一页的数据。当用户到达列表末尾且请求仍在进行中时,应显示加载视图。
基于上面的定义,让我们实现一个解决方案来解决这些问题,给List增加分页功能
实现
在此节中,我们将介绍两种不同的方案。第一种将更为简单,第二种将更为高级用户喜欢。
第一种方法
最简单的方法就是监测当前item是否是最后一个。如果是,我们则触发一个异步请求去提取下一页的数据。
RandomAccessCollection+isLastItem
由于List支持RandomAccessCollection,我们可以创建一个extension并实现isLastItem 函数。Self关键词是必须的,它将限制extension的元素必须实现Identifable。
好了,上面这段文字没有深入研究过swift的朋友肯定要懵圈了。大家可以参考我之前文章,简单了解一下RandomAccessCollection 和Identifiable
- SwiftUI RandomAccessCollection 是什么如何用(2020)
- SwiftUI 基础之06 Identifiable 有什么用
- SwiftUI guard 是什么如何用(2020教程)
下面是代码
extension RandomAccessCollection where Self.Element: Identifiable {
func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = firstIndex(where: { $0.id.hashValue == item.id.hashValue }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
}
上面代码用于判断item是否为List的末尾。
该函数在集合中查找给定项目的索引。它使用id属性的哈希值(需要实现Identifiable协议)将其与列表中的其他项目进行比较。如果找到了项目索引,则意味着项目索引与结束索引之间的距离必须恰好为一(结束索引等于集合中当前项目的数量)。这样我们才能知道给定的项目是最后一个项目
为了代替hash值的比较,我们可以使用 type-erased wrapper AnyHashable来直接比较Hashable类型。
guard let itemIndex = firstIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
好了,基础的业务逻辑我们已经实现,下面我们来实现界面部分。
界面
如果滚动到List底部,我们可以一个List 更新事件。为了达到这个目标,我们可以在根视图新增一个onAppear修饰器(在例子中,我们根视图是VStack)。onAppear将随后调用listItemAppears函数。
如果当前遍历item是最后一个,然后等待视图将显示给用户。在例子中,我们就用简单的Text("Loading...")。
由于SwiftUI是声明式的,因此下面的代码不言自明,非常易读:
struct ListPaginationExampleView: View {
@State private var items: [String] = Array(0...24).map { "Item \($0)" }
@State private var isLoading: Bool = false
@State private var page: Int = 0
private let pageSize: Int = 25
var body: some View {
NavigationView {
List(items) { item in
VStack(alignment: .leading) {
Text(item)
if self.isLoading && self.items.isLastItem(item) {
Divider()
Text("Loading ...")
.padding(.vertical)
}
}.onAppear {
self.listItemAppears(item)
}
}
.navigationBarTitle("List of items")
.navigationBarItems(trailing: Text("Page index: \(page)"))
}
}
}
辅助函数listItemAppears内部检查给定的item是否为最后一个。如果是最后一项,则当前页面会增加,下一页的项目会添加到列表中。此外,我们通过isLoading变量跟踪加载状态,该变量定义何时显示加载视图。
extension ListPaginationExampleView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isLastItem(item) {
isLoading = true
/*
Simulated async behaviour:
Creates items for the next page and
appends them to the list after a short delay
*/
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
}
通过上面的代码,当前迭代中的项目是最后一个项目时,我们才获取项目的下一页。
完整代码
创建个data.swift用于处理数据问题
//
// data.swift
// Swift_pagination_01
//
// Created by cf on 2020/1/26.
// Copyright © 2020 cf. All rights reserved.
//
import Foundation
import SwiftUI
struct DemoItem: Identifiable {
let id = UUID()
var sIndex = 0
var page = 0
}
extension RandomAccessCollection where Self.Element: Identifiable {
func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = firstIndex(where: { $0.id.hashValue == item.id.hashValue }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
}
界面部分
//
// ContentView.swift
// Swift_pagination_01
//
// Created by cf on 2020/1/26.
// Copyright © 2020 cf. All rights reserved.
//
import SwiftUI
struct ContentView: View {
@State private var items: [DemoItem] = Array(0...24).map { DemoItem(sIndex: $0,page:0) }
@State private var isLoading: Bool = false
@State private var page: Int = 0
private let pageSize: Int = 25
var body: some View {
NavigationView {
List(items) { item in
VStack {
Text("page:\(item.page) item:\(item.sIndex)")
if self.isLoading && self.items.isLastItem(item) {
Divider()
Text("Loading ...")
.padding(.vertical)
}
}.onAppear {
self.listItemAppears(item)
}
}
.navigationBarTitle("List of items")
.navigationBarItems(trailing: Text("Page index: \(page)"))
}
}
}
extension ContentView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isLastItem(item) {
isLoading = true
/*
Simulated async behaviour:
Creates items for the next page and
appends them to the list after a short delay
*/
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
func getMoreItems(forPage: Int, pageSize: Int) -> [DemoItem]{
let sitems: [DemoItem] = Array(0...24).map { DemoItem(sIndex: $0,page:forPage) }
return sitems
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
最终效果
项目完成代码
https://github.com/zhishidapang/SwiftUI-List-Pagination
下一步工作
但这并不是真正的最佳用户体验,对吧?在实际应用中,如果要达到或超过定义的阈值,我们希望预加载下一页。此外,我们仅应在确实有必要时(即,如果请求花费的时间比预期的长),使用加载指示器中断用户。我认为,这将带来更好的用户体验。
考虑到这些用户体验的问题,让我们跳到第二种方法。
更多SwiftUI教程和代码关注专栏
- 请关注我的专栏 SwiftUI教程与源码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。