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教程和代碼關注專欄

相關文章
相關標籤/搜索