尽管我们可以访问List中的具体item,但是我们不知道List滚动到了当前哪个位置,也不知道我们到List末尾的距离。这些数据都是我们进行分页的基础。

Pagination(分页)对于每个人都有不同的含义,因此我们先给分页的目标做个明确定义:

在滚动过程中,List应提取并追加下一页的数据。当用户到达列表末尾且请求仍在进行中时,应显示加载视图。

基于上面的定义,让我们实现一个解决方案来解决这些问题,给List增加分页功能

实现

在此节中,我们将介绍两种不同的方案。第一种将更为简单,第二种将更为高级用户喜欢。

第一种方法

最简单的方法就是监测当前item是否是最后一个。如果是,我们则触发一个异步请求去提取下一页的数据。

RandomAccessCollection+isLastItem

由于List支持RandomAccessCollection,我们可以创建一个extension并实现isLastItem 函数。Self关键词是必须的,它将限制extension的元素必须实现Identifable。

好了,上面这段文字没有深入研究过swift的朋友肯定要懵圈了。大家可以参考我之前文章,简单了解一下RandomAccessCollection 和Identifiable

下面是代码

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

最终效果

SwiftUI 高级List分页与无限滚动之基础版

项目完成代码

https://github.com/zhishidapang/SwiftUI-List-Pagination

下一步工作

但这并不是真正的最佳用户体验,对吧?在实际应用中,如果要达到或超过定义的阈值,我们希望预加载下一页。此外,我们仅应在确实有必要时(即,如果请求花费的时间比预期的长),使用加载指示器中断用户。我认为,这将带来更好的用户体验。

考虑到这些用户体验的问题,让我们跳到第二种方法。

更多SwiftUI教程和代码关注专栏


iCloudEnd
36 声望10 粉丝

iOS & Mac OS 攻城师 (历史 & 金融 & 美食 爱好者)