Pagination with Firebase firestore - swift 4
Asked Answered
A

5

7

I'm trying to paginate data (infinitely scroll my tableview) using firestore. I've integrated the code google gives for pagination as best I can, but I'm still having problems getting the data to load in correctly.

The initial set of data loads into the tableview as wanted. Then, when the user hits the bottom of the screen, the next "x" amount of items are load in. But when the user hits the bottom of the screen the second time, the same "x" items are simply appended to the table view. The same items keep getting added indefinitely.

So its the initial 3 "ride" objects, followed by the next 4 "ride" objects repeating forever.

123 4567 4567 4567 4567...

How do I get the data to load in correctly?

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = scrollView.contentOffset.y
    let contentHeight = scrollView.contentSize.height

    if offsetY > contentHeight - scrollView.frame.height {
        // Bottom of the screen is reached
        if !fetchingMore {
            beginBatchFetch()
        }
    }
}

func beginBatchFetch() {
    // Array containing "Ride" objcets is "rides"

    fetchingMore = true

    // Database reference to "rides" collection
    let ridesRef = db.collection("rides")

    let first = ridesRef.limit(to: 3)

    first.addSnapshotListener { (snapshot, err) in
        if let snapshot = snapshot {
            // Snapshot isn't nil
            if self.rides.isEmpty {
                // rides array is empty (initial data needs to be loaded in).
                let initialRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
                self.rides.append(contentsOf: initialRides)
                self.fetchingMore = false
                DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
                    self.tableView.reloadData()
                })
                print("first rides loaded in")
            }
        } else {
            // Error
            print("Error retreiving rides: \(err.debugDescription)")
            return
        }

        // reference to lastSnapshot
        guard let lastSnapshot = snapshot!.documents.last else{
            // The collection is empty
            return
        }


        let next = ridesRef.limit(to: 4).start(afterDocument: lastSnapshot)

        next.addSnapshotListener({ (snapshot, err) in
            if let snapshot = snapshot {

                if !self.rides.isEmpty {

                    let newRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
                    self.rides.append(contentsOf: newRides)
                    self.fetchingMore = false
                    DispatchQueue.main.asyncAfter(deadline: .now() + 7, execute: {
                        self.tableView.reloadData()
                    })

                    print("new items")
                    return
                }
            } else {
                print("Error retreiving rides: \(err.debugDescription)")
                return
            }

        })
    }
}
Aesculapius answered 5/9, 2018 at 19:19 Comment(0)
A
20

So here's the solution I've come up with! It is very likely that this solution makes multiple calls to firestore, creating a large bill for any real project, but it works as a proof of concept I guess you could say.

If you have any recommendations or edits, please feel free to share!

var rides = [Ride]()
var lastDocumentSnapshot: DocumentSnapshot!
var fetchingMore = false

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = scrollView.contentOffset.y
    let contentHeight = scrollView.contentSize.height
    //print("offsetY: \(offsetY) | contHeight-scrollViewHeight: \(contentHeight-scrollView.frame.height)")
    if offsetY > contentHeight - scrollView.frame.height - 50 {
        // Bottom of the screen is reached
        if !fetchingMore {
            paginateData()
        }
    }
}

// Paginates data
func paginateData() {
    
    fetchingMore = true
    
    var query: Query!
    
    if rides.isEmpty {
        query = db.collection("rides").order(by: "price").limit(to: 6)
        print("First 6 rides loaded")
    } else {
        query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
        print("Next 4 rides loaded")
    }
    
    query.getDocuments { (snapshot, err) in
        if let err = err {
            print("\(err.localizedDescription)")
        } else if snapshot!.isEmpty {
            self.fetchingMore = false
            return
        } else {
            let newRides = snapshot!.documents.compactMap({Ride(dictionary: $0.data())})
            self.rides.append(contentsOf: newRides)
            
            //
            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                self.tableView.reloadData()
                self.fetchingMore = false
            })
            
            self.lastDocumentSnapshot = snapshot!.documents.last
        }
    }
}
Aesculapius answered 7/9, 2018 at 7:1 Comment(0)
I
12

A little late in the game, but I would like to share how I do it, using the query.start(afterDocument:) method.

class PostsController: UITableViewController {

    let db = Firestore.firestore()

    var query: Query!
    var documents = [QueryDocumentSnapshot]()
    var postArray = [Post]()

    override func viewDidLoad() {
        super.viewDidLoad()

        query = db.collection("myCollection")
                  .order(by: "post", descending: false)
                  .limit(to: 15)

        getData()
    }

    func getData() {
        query.getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                querySnapshot!.documents.forEach({ (document) in
                    let data = document.data() as [String: AnyObject]

                    //Setup your data model

                    let postItem = Post(post: post, id: id)

                    self.postArray += [postItem]
                    self.documents += [document]
                })
                self.tableView.reloadData()
            }
        } 
    }

    func paginate() {
        //This line is the main pagination code.
        //Firestore allows you to fetch document from the last queryDocument
        query = query.start(afterDocument: documents.last!)
        getData()
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return postArray.count
    }

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        // Trigger pagination when scrolled to last cell
        // Feel free to adjust when you want pagination to be triggered
        if (indexPath.row == postArray.count - 1) {
            paginate()
        }
    }
}

Result like so:

enter image description here

Here is a reference.

Infrastructure answered 2/5, 2020 at 14:2 Comment(2)
Best answer I've seen for manual UITableView pagination.Fugal
This is a great answer. I had issues with WillDisplay as used here, so I opted to use scrollViewDidScroll instead (example: https://mcmap.net/q/210477/-how-to-know-when-uitableview-did-scroll-to-bottom-in-iphone) with the addition of a small delay to avoid duplication on bottom detection, but this is a great foundation.Cottager
M
0

My solution was similar to @yambo, however, I tried to avoid making extra calls to the database. After the first call to the database, I get back 10 objects and when it is time to load the new page I kept a reference of how many objects and I checked if the count + 9 is in the range of my new count.

    @objc func LoadMore() {
    let oldCount = self.uploads.count
    guard shouldLoadMore else { return }
    self.db.getNextPage { (result) in
        switch result {
        case .failure(let err):
            print(err)
        case .success(let newPosts):
            self.uploads.insert(contentsOf: newPosts, at: self.uploads.count)
            if oldCount...oldCount+9 ~= self.uploads.count {
                self.shouldLoadMore = false
            }
            DispatchQueue.main.async {
                self.uploadsView.collectionView.reloadData()
            }
        }
    }
}
Misgiving answered 12/2, 2020 at 5:37 Comment(0)
C
0

Latest Swift 5 using async await.

This is the full version of @Koh answer which includes the isFetching variable to stop unnecessary re-fetchings when we are already fetching FireStore. Also, we check if the snapshot.docs is empty so if it's empty we don't need to reload the collectionview/tableview/diffableDatasource whatever you are using.

Even though the Firebase video where they explain the pagination doesn't treat the case where you don't need the snapshot listeners we had great help with @Koh answer.

@Parth Mandavia answer couldn't be more wrong, doing that will cost you the same as if you had no pagination.

var isFetching = false
let order = "timestamp"
var query: Query!
var documents = [QueryDocumentSnapshot]()
let db = Firestore.firestore()

var petsArr = [Pet]()

func fetchPets(collection path: String) {
    guard !isFetching else { return }
    
    Task { [weak self] in
        guard let self = self else { return }

        isFetching = true
        state.send(.loading)
        
        do {
            if query == nil {
                query = db.collection(path)
                          .order(by: order, descending: true)
                          .limit(to: 10)
            } else {
                query = query.start(afterDocument: documents.last!)
            }
            
            
            let snapshot = try await self.query.getDocuments()
            
            let docs = snapshot.documents

             if !docs.isEmpty {
                for doc in docs {
                    let dictionary = doc.data()
                    let pet = Pet(dictionary: dictionary)
                    petsArr.append(pet)
                    documents.append(doc)
                }
            
                petsSubject.send(petsArr)
            }
            
        } catch {
            state.send(.error(.default(error)))
        }

      isFetching = false

    }
}
Comitative answered 10/10, 2023 at 6:26 Comment(0)
P
-1

Simple, Fast and easy way is...

class FeedViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FeedCellDelegate {

private var quotes = [Quote]() {
    
    didSet{ tbl_Feed.reloadData() }
}

var quote: Quote?
var fetchCount = 10

@IBOutlet weak var tbl_Feed: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    fetchPost() 
}


// MARK: - API

func fetchPost() {
    
    reference(.Quotes).limit(to: getResultCount).getDocuments { (snapshot, error) in
        
        guard let documents = snapshot?.documents else { return }
        
        documents.forEach { (doc) in
            
            let quotes = documents.map {(Quote(dictionary: $0.data()))}
            
            self.quotes = quotes
        }
    }
}  

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    
    let currentOffset = scrollView.contentOffset.y
    let maxxOffset = scrollView.contentSize.height - scrollView.frame.size.height
    
    if maxxOffset - currentOffset <= 300 { // Your cell size 300 is example

        fetchCount += 5
        fetchPost()

        print("DEBUG: Fetching new Data")
    }
}

}

Partlet answered 14/12, 2020 at 11:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.