0

I am trying to use Firestore pagination with swift TableView. I used the outline of the code provided by Google in their Firestore docs. Here is my code which loads the first 4 posts from Firestore.

func loadMessages(){
        let postDocs = db
            .collectionGroup("userPosts")
            .order(by: "postTime", descending: false)
            .limit(to: 4)

        postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
            self?.q.async{
                self!.posts = []

                guard let snapshot = querySnapshot else {
                    if let error = error {
                        print(error)
                    }
                    return
                }

                guard let lastSnapshot = snapshot.documents.last else {
                    // The collection is empty.
                    return
                }
                //where do I use this to load the next 4 posts?
                let nextDocs = Firestore.firestore()
                    .collectionGroup("userPosts")
                    .order(by: "postTime", descending: false)
                    .start(afterDocument: lastSnapshot)

                if let postsTemp = self?.createPost(snapshot){

                    DispatchQueue.main.async {
                        self!.posts = postsTemp
                        self!.tableView.reloadData()
                    }
                }
            }
        }
    }

    func createPost(_ snapshot: QuerySnapshot) ->[Post]{
        var postsTemp = [Post]()
        for doc in snapshot.documents{
            if let firstImage = doc.get(K.FStore.firstImageField) as? String,
                let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
                let secondImage = doc.get(K.FStore.secondImageField) as? String,
                let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
                let userName = doc.get(K.FStore.poster) as? String,
                let uID = doc.get(K.FStore.userID) as? String,
                let postDate = doc.get("postTime") as? String,
                let votesForLeft = doc.get("votesForLeft") as? Int,
                let votesForRight = doc.get("votesForRight") as? Int,
                let endDate = doc.get("endDate") as? Int{
                let post = Post(firstImageUrl: firstImage,
                                secondImageUrl: secondImage,
                                firstTitle: firstTitle,
                                secondTitle: secondTitle,
                                poster: userName,
                                uid: uID,
                                postDate: postDate,
                                votesForLeft: votesForLeft,
                                votesForRight:votesForRight,
                                endDate: endDate)
                postsTemp.insert(post, at: 0)
            }else{

            }
        }
        return postsTemp
    }

Here is my delegate which also detects the end of the TableView:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let post = posts[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
        cell.delegate = self

        let seconds = post.endDate
        let date = NSDate(timeIntervalSince1970: Double(seconds))
        let formatter = DateFormatter()
        formatter.dateFormat = "M/d h:mm"

        if(seconds <= Int(Date().timeIntervalSince1970)){
            cell.timerLabel?.text = "Voting Done!"
        }else{
            cell.timerLabel?.text = formatter.string(from: date as Date)
        }

        let firstReference = storageRef.child(post.firstImageUrl)
        let secondReference = storageRef.child(post.secondImageUrl)

        cell.firstTitle.setTitle(post.firstTitle, for: .normal)
        cell.secondTitle.setTitle(post.secondTitle, for: .normal)
        cell.firstImageView.sd_setImage(with: firstReference)
        cell.secondImageView.sd_setImage(with: secondReference)
        cell.userName.setTitle(post.poster, for: .normal)
        cell.firstImageView.layer.cornerRadius = 8.0
        cell.secondImageView.layer.cornerRadius = 8.0

        if(indexPath.row + 1 == posts.count){
            print("Reached the end")
        }

        return cell
    }

Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.

2 Answers 2

1

I’m assuming that your method of detecting when user reaches the bottom of the tableView items is correct.

In my personal opinion setting real-time listeners for pagination would be quite a challenge. I recommend you using a bunch of get calls to do this.

If done in that way, what you need is a function that every time it’s called, it brings the next set of posts. For example, first time it’s called, it’ll fetch 4 latest docs A.K.A posts. Second time it’s called, it’ll fetch the next latest set of posts (4). To clarify the resulting posts from first call is newer than second call. Hopefully this is making sense.

How to? Maintain two properties, one that keeps track of last document fetched, And one that stores all the posts fetched up to now(array or any applicable data structures). If the function gets called 4 times the array I’m talking about here would have 16posts (provided that there are >= 16 posts in firestore).

Now since we have the point to which we fetched the posts from firestore now, we can use the Firestore API to configure the query to fetch the next set, first call onwards. Each time a set of documents/posts is received it’s appended to the array.

Oh almost forgot, the function I’m speaking of here, has to be called every time the User reaches tableView end.

This solution may or may not be ideal for you, but hopefully it at-least leads you down some path to finding a solution. Any questions are welcome, happy to help..

Sign up to request clarification or add additional context in comments.

Comments

1
//I have this solution working in a project, the approach is to detect when the user scrolls and the offset is getting close to the top
//When this happens, you get the next bunch of elements from firestore, insert them in your data source and finallly reload the tableview keeping the scroll offset.
//below are the related methods, hope it helps.

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if scrollView.contentOffset.y < 300{
        self.stoppedScrolling()
    }
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        if scrollView.contentOffset.y < 300{
            self.stoppedScrolling()
        }
    }
}

//When the tableview stops scrolling you call your method getNextPosts which should be very similar to your loadMessages, maybe you dont need a listener, you just need the next posts.
func stoppedScrolling() {
    getNextPosts { posts in
        self.insertNextPosts(posts)
    }
}

//Insert the new messages that you just got
private func insertNextPosts(_ posts: [Post]){
    self.messages.insert(contentsOf: posts, at: 0)
    self.messagesCollectionView.reloadDataAndKeepOffset()
}

//This function es from MessageKit: https://messagekit.github.io, take it only as reference, besides is for a collectionview but you can adapt it to tableview
public func reloadDataAndKeepOffset() {
    // stop scrolling
    setContentOffset(contentOffset, animated: false)

    // calculate the offset and reloadData
    let beforeContentSize = contentSize
    reloadData()
    layoutIfNeeded()
    let afterContentSize = contentSize

    // reset the contentOffset after data is updated
    let newOffset = CGPoint(
        x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
        y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
    setContentOffset(newOffset, animated: false)
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.