SwiftUI 高级List分页与无限滚动之基础版(2020教程)

尽管咱们能够访问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()
    }
}


复制代码

最终效果

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

项目完成代码

github.com/zhishidapan…

下一步工做

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

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

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

相关文章
相关标签/搜索