Tarvo Mäesepp Tarvo Mäesepp - 4 months ago 44
Swift Question

Pagination while scrolling in Swift Firebase

I am loading data from Firebase Database. I am trying to load data while user scrolls. For example if he has scrolled about 20

cells
then I want to load a little more about 5
cells
. I am trying to achieve it like this but it is not working:

func parseSnusBrands(){
let ref = FIRDatabase.database().reference().child("Snuses").child("Brands")

ref.queryOrderedByKey().queryLimitedToLast(20).observeEventType(.Value, withBlock: { (snapshot) in
if snapshot.exists() {
if let all = (snapshot.value?.allKeys)! as? [String]{
self.snusBrandsArray = all
self.snusBrandsTableView.reloadData()

}
}
})
}


and to detect how many cells are scrolled:

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 20 {
let ref = FIRDatabase.database().reference().child("Snuses").child("Brands")

ref.queryOrderedByKey().queryLimitedToLast(20).observeEventType(.Value, withBlock: { (snapshot) in
if snapshot.exists() {
if let all = (snapshot.value?.allKeys)! as? [String]{
self.snusBrandsArray = all
self.snusBrandsTableView.reloadData()

}
}
})
}
}


This is my Firebase structure:

enter image description here

Is there better solution or should I keep trying like this?

Answer

You may want to use offset to achieve pagination in your app.

First, add an order key in your child path, may be current time stamp of insertion so that you can order it and use it in pagination.

For eg.

"brands": {
         "catch" : {
           ...
           ...
           "pOrder" : 555
         },
         "etan" : {
           ...
           ...
           "pOrder" : 444
         },
         "general" : {
           ...
           ...
           "pOrder" : 555
         }.
         .....
       }

Now, to achieve pagination you would retrieve 21 records and in those records, the first 20 will be the records to use in table view, while the last record will be the offset that will be used to retrieve next set of records.

Create a function that fetches data from firebase:

 // MARK: Retrieving Data: Firebase
    /*
     retrieve current set of posts sorted by updated time of posts
     */
    func retrievePost(offset: NSNumber, callFlag: Bool, completion: (result: AnyObject?, error: NSError?)->()){
        // As this method is called from viewDidLoad and fetches 20 records at first.
        // Later when user scrolls down to bottom, its called again
        let postsRef = ref.child(kDBPostRef)
        var startingValue:AnyObject?
        // starting  value will be nil when this method is called from viewDidLoad as the offset is not set

        if callFlag{
            if offset == 0{
                startingValue = nil
            }
            else{
                startingValue = offset
            }
        } else{
            // get offset from the offsetArray
            startingValue = self.findOffsetFromArray()

        }
        // sort records by pOrder fetch offset+1 records
        self.refHandler = postsRef.queryOrderedByChild("pOrder").queryStartingAtValue(startingValue).queryLimitedToFirst(kPostLimit + 1).observeEventType(FIRDataEventType.Value, withBlock: { (snapshot) in
            // flag is for setting the last record/ 21st as offset
            var flag = 0

            let tempPost = NSMutableSet()
                // iterate over children and add to tempPost
                for item in snapshot.children {

                    // check for offet, the last row(21st) is offset ; Do not add last element in the main table list
                    flag += 1
                    if flag == 21 && callFlag == true{
                        // this row is offset
                        self.kOffset = item.value?["pOrder"] as! NSNumber
                        self.offSetArray?.append(self.kOffset)
                        continue
                    }
                    // create Post object
                    let post = Post(snapshot: item as! FIRDataSnapshot)

                    // append to tempPost
                    tempPost.addObject(post)
                }
                // return to the closure
                completion(result:tempPost, error:nil)
        })
    }

And call this updateNewRecords method from viewDidLoad, passing 0 to retrieve first 20 records.

 func updateNewRecords(offset:NSNumber, callFlag: Bool){
        self.retrievePost(offset,callFlag:callFlag) { (result,error) -> Void in
//            let tempArray = result as! [Post]
            let oldSet = Set(self.posts)
            var unionSet = oldSet.union(result as! Set<Post>)
            unionSet = unionSet.union(unionSet)
            self.posts = Array(unionSet)
            self.postsCopy = self.posts
//          print(self.posts.count)
            self.posts.sortInPlace({ $0.pOrder> $1.pOrder})
            self.reloadTableData()
        }
    }

Again when the user scrolls to bottom in the table view, call this method, updateNewRecords passing the offset, to retrieve next set of 20 records.

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {

        // UITableView only moves in one direction, y axis
        let currentOffset = scrollView.contentOffset.y
        let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height

        // Change 10.0 to adjust the distance from bottom
        if maximumOffset - currentOffset <= 10.0 {
            self.updateNewRecords(self.kOffset, callFlag:true)
        }
    } 

You can maintain an array offsetArray for updating table values when your data is modified in firebase .

For e.g. suppose some data is modified somewhere in your table, in that case observeEventType will be called and you should fetch only those range of records from firebase. You can achieve it using maintaining an array of offsets. So when any record is updated, the retrievePost will be called with the corresponding offset from offsetArray. In normal case, ( while retrieving data from viewDidLoad and scrollViewDidEndDragging), the offset will be kOffset and in rest of the cases(while listening to data change), the offset will be from offsetArray.

You can define a function that will return the offset value for a particular updated column:

// find the offset from the offsetDict
    func findOffsetFromArray() -> NSNumber{
        let idx = self.kClickedRow/20 // kClickedRow is the updated row in the table view
        return self.offSetArray![idx]

    }

You can also modify retrievePost method and pass an extra parameter to retrieve a certain number of records.

Comments