List 对象的 SwiftUI 分页
SwiftUI pagination for List object
我在 SwiftUI 中实现了一个带有搜索栏的列表。现在我想为这个列表实现分页。当用户滚动到列表底部时,应该加载新元素。我的问题是,如何检测到用户滚动到末尾?发生这种情况时,我想加载新元素,附加它们并将它们显示给用户。
我的代码如下所示:
import Foundation
import SwiftUI
struct MyList: View {
@EnvironmentObject var webService: GetRequestsWebService
@ObservedObject var viewModelMyList: MyListViewModel
@State private var query = ""
var body: some View {
let binding = Binding<String>(
get: { self.query },
set: { self.query = [=10=]; self.textFieldChanged([=10=]) }
)
return NavigationView {
// how to detect here when end of the list is reached by scrolling?
List {
// searchbar here inside the list element
TextField("Search...", text: binding) {
self.fetchResults()
}
ForEach(viewModelMyList.items, id: \.id) { item in
MyRow(itemToProcess: item)
}
}
.navigationBarTitle("Title")
}.onAppear(perform: fetchResults)
}
private func textFieldChanged(_ text: String) {
text.isEmpty ? viewModelMyList.fetchResultsThrottelt(for: nil) : viewModelMyList.fetchResultsThrottelt(for: text)
}
private func fetchResults() {
query.isEmpty ? viewModelMyList.fetchResults(for: nil) : viewModelMyList.fetchResults(for: query)
}
}
这种情况也有点特殊,因为列表包含搜索栏。如果有任何建议,我将不胜感激 :).
将 .onAppear()
添加到 MyRow
并让它使用刚刚出现的项目调用 viewModel。然后您可以检查它是否等于列表中的最后一项,或者它是否远离列表末尾的 n 项并触发您的分页。
因为您已经有了一个带有搜索栏人工行的 List
,您可以简单地向列表中添加另一个视图,当它出现在屏幕上时将触发另一个提取(使用 onAppear()
正如乔希所建议的那样)。通过这样做,您不必进行任何 "complicated" 计算即可知道某一行是否是最后一行...人工行始终是最后一行!
我已经在我的一个项目中使用过它,但我从未在屏幕上看到过这个元素,因为加载在它出现在屏幕上之前就被触发得如此之快。 (您当然可以使用 transparent/invisible 元素,或者甚至可以使用微调器 ;-))
List {
TextField("Search...", text: binding) { /* ... */ }
ForEach(viewModelMyList.items, id: \.id) { item in
// ...
}
if self.viewModelMyList.hasMoreRows {
Text("Fetching more...")
.onAppear(perform: {
self.viewModelMyList.fetchMore()
})
}
这个对我有用:
您可以使用两种不同的方法为您的列表添加分页:最后一项方法和阈值项目方法。
这就是这个包向 RandomAccessCollection 添加两个函数的方式:
是最后一项
使用此函数检查当前列表项迭代中的项是否是您 collection 的最后一项。
isThresholdItem
使用此功能,您可以查明当前列表项迭代的项目是否是您定义的阈值的项目。将偏移量(到最后一项的距离)传递给函数,以便可以确定阈值项。
import SwiftUI
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([=10=].id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
public func isThresholdItem<Item: Identifiable>(
offset: Int,
item: Item
) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable([=10=].id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
let offset = offset < count ? offset : count - 1
return offset == (distance - 1)
}
}
例子
最后一项方法:
struct ListPaginationExampleView: View {
@State private var items: [String] = Array(0...24).map { "Item \([=11=])" }
@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)"))
}
}
}
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
}
}
}
}
门槛项进近:
struct ListPaginationThresholdExampleView: View {
@State private var items: [String] = Array(0...24).map { "Item \([=12=])" }
@State private var isLoading: Bool = false
@State private var page: Int = 0
private let pageSize: Int = 25
private let offset: Int = 10
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)"))
}
}
}
extension ListPaginationThresholdExampleView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isThresholdItem(offset: offset,
item: 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() + 0.5) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
}
字符串扩展:
/*
If you want to display an array of strings
in the List view you have to specify a key path,
so each string can be uniquely identified.
With this extension you don't have to do that anymore.
*/
extension String: Identifiable {
public var id: String {
return self
}
}
我在 SwiftUI 中实现了一个带有搜索栏的列表。现在我想为这个列表实现分页。当用户滚动到列表底部时,应该加载新元素。我的问题是,如何检测到用户滚动到末尾?发生这种情况时,我想加载新元素,附加它们并将它们显示给用户。
我的代码如下所示:
import Foundation
import SwiftUI
struct MyList: View {
@EnvironmentObject var webService: GetRequestsWebService
@ObservedObject var viewModelMyList: MyListViewModel
@State private var query = ""
var body: some View {
let binding = Binding<String>(
get: { self.query },
set: { self.query = [=10=]; self.textFieldChanged([=10=]) }
)
return NavigationView {
// how to detect here when end of the list is reached by scrolling?
List {
// searchbar here inside the list element
TextField("Search...", text: binding) {
self.fetchResults()
}
ForEach(viewModelMyList.items, id: \.id) { item in
MyRow(itemToProcess: item)
}
}
.navigationBarTitle("Title")
}.onAppear(perform: fetchResults)
}
private func textFieldChanged(_ text: String) {
text.isEmpty ? viewModelMyList.fetchResultsThrottelt(for: nil) : viewModelMyList.fetchResultsThrottelt(for: text)
}
private func fetchResults() {
query.isEmpty ? viewModelMyList.fetchResults(for: nil) : viewModelMyList.fetchResults(for: query)
}
}
这种情况也有点特殊,因为列表包含搜索栏。如果有任何建议,我将不胜感激 :).
将 .onAppear()
添加到 MyRow
并让它使用刚刚出现的项目调用 viewModel。然后您可以检查它是否等于列表中的最后一项,或者它是否远离列表末尾的 n 项并触发您的分页。
因为您已经有了一个带有搜索栏人工行的 List
,您可以简单地向列表中添加另一个视图,当它出现在屏幕上时将触发另一个提取(使用 onAppear()
正如乔希所建议的那样)。通过这样做,您不必进行任何 "complicated" 计算即可知道某一行是否是最后一行...人工行始终是最后一行!
我已经在我的一个项目中使用过它,但我从未在屏幕上看到过这个元素,因为加载在它出现在屏幕上之前就被触发得如此之快。 (您当然可以使用 transparent/invisible 元素,或者甚至可以使用微调器 ;-))
List {
TextField("Search...", text: binding) { /* ... */ }
ForEach(viewModelMyList.items, id: \.id) { item in
// ...
}
if self.viewModelMyList.hasMoreRows {
Text("Fetching more...")
.onAppear(perform: {
self.viewModelMyList.fetchMore()
})
}
这个对我有用:
您可以使用两种不同的方法为您的列表添加分页:最后一项方法和阈值项目方法。
这就是这个包向 RandomAccessCollection 添加两个函数的方式:
是最后一项
使用此函数检查当前列表项迭代中的项是否是您 collection 的最后一项。
isThresholdItem
使用此功能,您可以查明当前列表项迭代的项目是否是您定义的阈值的项目。将偏移量(到最后一项的距离)传递给函数,以便可以确定阈值项。
import SwiftUI
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([=10=].id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
public func isThresholdItem<Item: Identifiable>(
offset: Int,
item: Item
) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable([=10=].id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
let offset = offset < count ? offset : count - 1
return offset == (distance - 1)
}
}
例子
最后一项方法:
struct ListPaginationExampleView: View {
@State private var items: [String] = Array(0...24).map { "Item \([=11=])" }
@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)"))
}
}
}
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
}
}
}
}
门槛项进近:
struct ListPaginationThresholdExampleView: View {
@State private var items: [String] = Array(0...24).map { "Item \([=12=])" }
@State private var isLoading: Bool = false
@State private var page: Int = 0
private let pageSize: Int = 25
private let offset: Int = 10
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)"))
}
}
}
extension ListPaginationThresholdExampleView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isThresholdItem(offset: offset,
item: 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() + 0.5) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
}
字符串扩展:
/*
If you want to display an array of strings
in the List view you have to specify a key path,
so each string can be uniquely identified.
With this extension you don't have to do that anymore.
*/
extension String: Identifiable {
public var id: String {
return self
}
}