尽管咱们能够访问List中的具体item,可是咱们不知道List滚动到了当前哪一个位置,也不知道咱们到List末尾的距离。这些数据都是咱们进行分页的基础。git
Pagination(分页)对于每一个人都有不一样的含义,所以咱们先给分页的目标作个明肯定义:github
在滚动过程当中,List应提取并追加下一页的数据。当用户到达列表末尾且请求仍在进行中时,应显示加载视图。swift
基于上面的定义,让咱们实现一个解决方案来解决这些问题,给List增长分页功能bash
在此节中,咱们将介绍两种不一样的方案。第一种将更为简单,第二种将更为高级用户喜欢。app
最简单的方法就是监测当前item是不是最后一个。若是是,咱们则触发一个异步请求去提取下一页的数据。dom
RandomAccessCollection+isLastItem
复制代码
因为List支持RandomAccessCollection,咱们能够建立一个extension并实现isLastItem 函数。Self关键词是必须的,它将限制extension的元素必须实现Identifable。异步
好了,上面这段文字没有深刻研究过swift的朋友确定要懵圈了。你们能够参考我以前文章,简单了解一下RandomAccessCollection 和Identifiableasync
下面是代码ide
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()
}
}
复制代码
但这并非真正的最佳用户体验,对吧?在实际应用中,若是要达到或超过定义的阈值,咱们但愿预加载下一页。此外,咱们仅应在确实有必要时(即,若是请求花费的时间比预期的长),使用加载指示器中断用户。我认为,这将带来更好的用户体验。
考虑到这些用户体验的问题,让咱们跳到第二种方法。