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

在《SwiftUI 高级List分页与无限滚动之基础版(2020教程)》中我们介绍了List分页的基础实现方法,但是这种方法没有站在用户的角度思考,下面我们以用户为中心制作分页。

下面,我们将研究一下如何通过阈值控制获取下一页时间。

RandomAccessCollection+isThresholdItem

这次,我们将从extentions的RandomAccessCollection开始。这次,我们将实现一个名为isThresholdItem的函数,该函数确定给定item是否为阈值item。

extension RandomAccessCollection where Self.Element: Identifiable {
    func isThresholdItem<Item: Identifiable>(offset: Int,
                                             item: Item) -> Bool {
        guard !isEmpty else {
            return false
        }
        
        guard let itemIndex = firstIndex(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)
    }
}

此函数查找给定项目的索引。如果找到,它将计算到终点索引的距离。指定的偏移量(即,结束之前的项目数)应等于distance —1。我们必须从该距离中减去1,因为结束索引等于count属性的值(即,集合中的当前项目数) )。我还为偏移量添加了一个简单的验证检查。偏移量应小于集合中的当前项目数.

界面部分

UI实现与第一种方法中的UI几乎相同。但是,有一个关键的区别,那就是listItemAppears函数

请记住,这里我们从第一种方法重用了isLastItem函数。仅当用户到达列表的末尾并且对下一页的请求仍在进行中时,才会显示加载视图

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

代替调用isLastItem,我们调用isThresholdItem来检查给定项目是否为阈值项目。

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

如果您是一位特别专心的读者,您可能会注意到缺少一些代码。让我们进入下一部分。

载入更多的数据

extension ListPaginationExampleView {
    /*
        In a real app you would probably fetch data
        from an external API.
     */
    private func getMoreItems(forPage page: Int,
                              pageSize: Int) -> [String] {
        let maximum = ((page * pageSize) + pageSize) - 1
        let moreItems: [String] = Array(items.count...maximum).map { "Item \($0)" }
        return moreItems
    }
}

String 实现Identifiable

/*
   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.
*/
extension String: Identifiable {
   public var id: String {
       return self
   }
}

好的,两种方法都给大家介绍了。我先去睡个午觉,下午把整个项目放到github上

项目完整代码

//
//  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
    }
    func isThresholdItem<Item: Identifiable>(offset: Int,
                                             item: Item) -> Bool {
        guard !isEmpty else {
            return false
        }
        
        guard let itemIndex = firstIndex(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)
    }
}

//
//  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
    private let offset: Int = 10
    
    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.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
            }
        }
    }
    /*
    func getMoreItems(forPage: Int, pageSize: Int) -> [DemoItem]{
        let sitems: [DemoItem] = Array(0...24).map { DemoItem(sIndex: $0,page:forPage) }
        return sitems
    }
 */
    private func getMoreItems(forPage page: Int,
                                pageSize: Int) -> [DemoItem] {
          let maximum = ((page * pageSize) + pageSize) - 1
          let moreItems: [DemoItem] = Array(items.count...maximum).map { DemoItem(sIndex: $0,page:page)  }
          return moreItems
      }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


项目下载地址

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

推荐阅读更多精彩内容