Although we can access specific items in the List, we don’t know where the List is currently scrolling or how far we are to the end of the List. This data is the basis of our pagination.

Pagination means something different to everyone, so let’s define the goal of Pagination:

As you scroll, the List should extract and append the data for the next page. When the user reaches the end of the list and the request is still in progress, the load view should be displayed.

Based on the above definition, let’s implement a solution to solve these problems by adding pagination to the List

implementation

In this section, we introduce two different scenarios. The first will be simpler, and the second will be more palatable to power users.

The first way

The easiest way to do this is to check if the current item is the last one. If so, we trigger an asynchronous request to extract the data for the next page.

RandomAccessCollection+isLastItem
Copy the code

Since List supports RandomAccessCollection, we can create an extension and implement the isLastItem function. The Self keyword is required, which restricts the extension element to implement Identifable.

Well, for those of you who haven’t studied Swift in depth, the above paragraph is going to be confusing. Refer to my previous article for a brief look at RandomAccessCollection and similarly

  • SwiftUI RandomAccessCollection SwiftUI RandomAccessCollection
  • SwiftUI Basics 06IdentifiableWhat’s the use of
  • How to use SwiftUI Guard

Here’s the code

extension RandomAccessCollection whereSelf.Element: Identifiable { func isLastItem<Item: Identifiable>(_ item: Item) -> Bool { guard ! isEmptyelse {
            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
    }
}
Copy the code

The above code is used to determine if item is the end of the List.

This function looks up the index of a given item in the collection. It compares it to other items in the list using the hash value of the ID attribute (requiring an Identifiable agreement). If the item index is found, it means that the distance between the item index and the end index must be exactly one (the end index is equal to the number of current items in the collection). This is how we know that a given item is the last item

In lieu of comparing hash values, we can use the Type-erased wrapper AnyHashable to compare the Hashable types directly.

guard let itemIndex = firstIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
    return false
}
Copy the code

Now that we’ve implemented the basic business logic, let’s implement the interface part.

interface

If we scroll down to the bottom of the List, we can have a List update event. To do this, we can add an onAppear decorator to the root view (in our case, VStack). OnAppear will then call the listItemAppears function.

If the current traversal item is the last, then wait for the view to be displayed to the user. In our example, we use simple Text(“Loading…”). ).

Since SwiftUI is declarative, the following code is self-explanatory and very readable:

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)"))}}}Copy the code

The auxiliary function listItemAppears checks internally whether the given item is the last. If it is the last item, the current page is added and the item on the next page is added to the list. In addition, we track the loading state through the isLoading variable, which defines when the loading view is displayed.

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}}}}Copy the code

With the code above, we only get the next page of the project when the item in the current iteration is the last item.

The complete code

Create a data.swift to handle data problems

//
//  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 whereSelf.Element: Identifiable { func isLastItem<Item: Identifiable>(_ item: Item) -> Bool { guard ! isEmptyelse {
            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
    }
}

Copy the code

Interface part

// // 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]{
        letsitems: [DemoItem] = Array(0... 24).map { DemoItem(sIndex:$0,page:forPage) }
        return sitems
    }
}

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


Copy the code

The final result

Project completion code

Github.com/zhishidapan…

Next step

But that’s not really the best user experience, right? In practice, if a defined threshold is to be reached or exceeded, we want to preload the next page. In addition, we should interrupt users using load indicators only when it is truly necessary (that is, if the request is taking longer than expected). I think this will lead to a better user experience.

With these user experience issues in mind, let’s jump to the second approach.

More SwiftUI tutorials and code focus columns

  • Please follow my SwiftUI tutorial column with source code