14

I have this Firebase data:

Firebase Data

I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code

let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....

  })

When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"

What am I missing here?

Community
  • 1
  • 1
jhnferraris
  • 1,361
  • 1
  • 12
  • 34
  • There's a logic issue. For page: 1, count: 1, the query starts at 0 and returns 1 post. For page: 2, count: 1, the query starts at 1 and return 1 post. Both based on the createdAt node which has values that are both way more then 1 so it will only ever return the first post. – Jay May 25 '16 at 12:44

2 Answers2

17

Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.

The unique key is based on a timestamp, so list items will automatically be ordered chronologically.

To start on a specific key, you will have to append your query with queryStartingAtValue(_:).

Sample usage:

var count = numberOfItemsPerPage

var query ref.queryOrderedByKey()

if startKey != nil {
  query = query.queryStartingAtValue(startKey)
  count += 1
}

query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
  guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
    // Handle error
    return
  }

  if startKey != nil && !children.isEmpty {
    children.removeFirst()
  }

  // Do something with children
})
timominous
  • 436
  • 3
  • 5
  • 1
    how about descending order? Having the newest data first? – Miroslav Kuťák Jun 06 '16 at 07:08
  • @KutakMir I think use: `queryEndingAtValue` with `queryLimitedToLast` – Tai Le Jun 22 '16 at 11:56
  • @KutakMir I think @levantAJ is right. You can use those methods but the key you will be passing should be different. Instead of using the key of the last item returned by Firebase, you will be using the key of the first item. Also instead of `removeFirst()`, use `removeLast()`. I can't test this now so you should try it for yourself. :) – timominous Jun 23 '16 at 14:36
  • 1
    i'm agree with @HimaliShah , what is startKey?. please answer it. – Vatsal Shukla May 17 '17 at 11:22
  • 1
    @Vats each snapshot has a key. (snapshot.key) this is the id of the record itself. All you do is save the first one or last one. :) – HelloimDarius May 18 '17 at 10:02
  • 2
    Please explain or show more code. If you guys don't mind... fight with firebase pagination is make me confused – Andrian Rahardja Jul 20 '17 at 03:44
6

I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.

Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.

Now, the model looks like so...

    class FeedsModel: Decodable {
        var postId: String!
        var authorId: String! //The author of the post
        var timestamp: Double = 0.0 //We'll use it sort the posts.
        //And other properties like 'likesCount', 'postDescription'...
    }

We're going to get the posts in the recent first fashion using this function

    class func getFeedsWith(lastKey: String?, completion: @escaping ((Bool, [FeedsModel]?) -> Void)) {
        let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
        let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
        //Last key would be nil initially(for the first page).

        query.observeSingleEvent(of: .value) { (snapshot) in
            guard snapshot.exists(), let value = snapshot.value else {
                completion(false, nil)
                return
            }
            do {
                let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
                //We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
                var feeds = model.map { $0.value }
                //Leaving the keys aside to get the array [FeedsModel]
                feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp }) 
                //Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
                if lastKey != nil { feeds = Array(feeds.dropFirst()) }
                //Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
                completion(true, feeds)
                //We get our data sorted and ready here.
            } catch let error {
                print("Error occured while decoding - \(error.localizedDescription)")
                completion(false, nil)
            }
        }
    }

Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...

    class FeedsViewController: UIViewController {

        //MARK: - Properties
        @IBOutlet weak var feedsTableView: UITableView!

        var dataArray = [FeedsModel]()
        var isFetching = Bool()
        var previousKey = String()
        var hasFetchedLastPage = Bool()


        //MARK: - ViewController LifeCycle
        override func viewDidLoad() {
            super.viewDidLoad()
            //Any other stuffs..
            self.getFeedsWith(lastKey: nil) //Initial load.
        }

        //....

        func getFeedsWith(lastKey: String?) {

            guard !self.isFetching else {
                self.previousKey = ""
                return
            }
            self.isFetching = true

            FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
                self.isFetching = false
                guard status, let feeds = data else {
                    //Handle errors
                    return
                }

                if self.dataArray.isEmpty { //It'd be, when it's the first time.
                    self.dataArray = feeds
                    self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
                } else {
                    self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
                    //To make sure if we've fetched the last page and we're in no need to call this function anymore.
                    self.dataArray += feeds
                   //Appending the next page's feed. As we're getting the feeds in the recent first manner.
                    self.feedsTableView.reloadData()
                }
            }
        }

        //MARK: - TableView Delegate & DataSource

        //....

        func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
            let lastKey = self.dataArray[indexPath.row].postId
            guard lastKey != self.previousKey else { return }
            //Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
            self.getFeedsWith(lastKey: lastKey)
            self.previousKey = lastKey ?? ""
        }

        //....
    }
Gaurav Chandarana
  • 738
  • 10
  • 10
  • In Swift with offline mode enabled ```Database.database().isPersistenceEnabled = true``` observeSingleEvent often returns cached data (not the latest data), does anyone have a solution with just observe? – Jack Chen May 12 '21 at 18:33
  • 1
    Firebase 7.5.0 seems to have fixed the issue of getting cached data when using isPersistenceEnabled. Use the newly introduced getData instead of observeSingleEvent. – Jack Chen May 13 '21 at 03:13
  • I think you got an error at `` self.previousKey = lastKey ?? "" `` . You are assigning the previous key to the last key, the condition won't met in the next cycle, I got duplicates in the second round. take a look @GauravChandarana ``` last key:("-N1OqP6pe3K5TDeuA9Q4") previous key :("-N1OqP6pe3K5TDeuA9Q4") ``` – David.C May 06 '22 at 20:53
  • 1
    Found the problem, it was ``feeds.dropFirst()`` needed to ``dropLast()`` instead, that's why I got the same keys in the next cycles – David.C May 06 '22 at 21:06