列表对象的SwiftUI分页

yqhsw0fo  于 2022-11-28  发布在  Swift
关注(0)|答案(3)|浏览(198)

我已经在SwiftUI中实现了一个带有搜索栏的List。现在我想为这个List实现分页。当用户滚动到列表的底部时,新元素应该被加载。我的问题是,我如何检测到用户滚动到了底部?当这种情况发生时,我想加载新元素,追加它们并向用户显示它们。
我的代码如下所示:

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 = $0; self.textFieldChanged($0) }
        )

        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)
    }
}

这种情况也有点特殊,因为列表包含搜索栏。我会感谢任何建议,因为有了这个:)。

epggiuax

epggiuax1#

由于您已经有了一个List,它带有一个用于搜索栏的人工行,您可以简单地向列表中添加另一个视图,当它出现在屏幕上时,该列表将触发另一次获取(使用Josh建议的onAppear())。通过这样做,您不必进行任何“复杂”的计算来知道一行是否是最后一行...人工行总是最后一行!
我已经在我的一个项目中使用了这个元素,但我从来没有在屏幕上看到过这个元素,因为加载在它出现在屏幕上之前就被触发了。(您当然可以使用透明/不可见的元素,或者甚至可以使用微调器;- ))

List {
    TextField("Search...", text: binding) {
        /* ... */
    }

    ForEach(viewModelMyList.items, id: \.id) { item in
        // ...
    }

    if self.viewModelMyList.hasMoreRows {
        Text("Fetching more...")
                .onAppear(perform: {
                    self.viewModelMyList.fetchMore()
                })
    }
}
xsuvu9jc

xsuvu9jc2#

MyRow中添加一个.onAppear(),并让它调用viewModel,然后检查它是否等于列表中的最后一项,或者它是否距离列表末尾有n项,并触发分页。

bt1cpqcv

bt1cpqcv3#

这个对我很有效:

您可以使用两种不同的方法为List添加分页:最后一项法和阈值项法。
这就是这个包向RandomAccessCollection添加两个函数的方式:

是最后一个项目

使用此函数检查当前List项迭代中的项是否是集合的最后一项。

是阈值项

使用此函数,您可以确定当前列表项迭代的项是否是您定义的阈值项。将偏移量(到最后一个项的距离)传递给函数,以便确定阈值项。

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($0.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($0.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 \($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)"))
        }
    }
}

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 \($0)" }
    @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
    }
}

Christian Elies, code reference

相关问题