Swift iOS: Firebase Paging

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 ?? ""
        }

        //....
    }

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
})