PFQueryTableViewController pagination doesn't work with heightForRowAtIndexPath

589 views Asked by At

I am using parse.com framework with Swift and in PFQueryTableViewController when I set the pagination it won't work. If the DB has less rows than the number set in objectPerPage it works fine, but if there are more rows and when I run the app it keeps showing the loading screen and nothing is downloaded, when I do "swipe as refresh" it crash as Error

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]

ImagesTableViewController.swift

import UIKit
import Parse
import ParseUI
import Bolts

class ImagesTableViewController: PFQueryTableViewController {
@IBAction func unwindToSegue (segue : UIStoryboardSegue) {}

// Initialise the PFQueryTable tableview
override init(style: UITableViewStyle, className: String!) {
    super.init(style: style, className: className)
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    // Configure the PFQueryTableView
    self.parseClassName = "Image"
    self.pullToRefreshEnabled = true
    self.paginationEnabled = true
    self.objectsPerPage = 5

}

// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery {
    var query = PFQuery(className: "Image")
    query.whereKey("deleted", notEqualTo: 1)
    query.orderByDescending("createdAt")
    return query
}

//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell {

    var cell = tableView.dequeueReusableCellWithIdentifier("ImageCell") as! ImageTVCell!
    if cell == nil {
        cell = ImageTVCell(style: UITableViewCellStyle.Default, reuseIdentifier: "ImageCell")
    }

    // Extract values from the PFObject to display in the table cell HEADLINE
    if let caption = object?["caption"] as? String {
        cell?.headlineLabel?.text = caption
    }

    // Display image
    var initialThumbnail = UIImage(named: "question")
    cell.postImageView.image = initialThumbnail
    if let thumbnail = object?["image"] as? PFFile {
        cell.postImageView.file = thumbnail
        cell.postImageView.loadInBackground()
    }

    return cell
}

// if I remove this code pagination work but the cell height is wrong
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return calculateHeightForRowAtIndexPath(indexPath)
}


func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
    if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
        println("Ratio: \(ratio)")
        return tableView.bounds.size.width / CGFloat(ratio)
    } else {
        return 50.0
    }
}


@IBAction func addNewPhotoButton(sender: UIBarButtonItem) {
    self.tabBarController?.tabBar.hidden = true
    self.performSegueWithIdentifier("showUploadNewImage", sender: self)
}

}
2

There are 2 answers

6
Kumuluzz On BEST ANSWER

This problem occurs because of PFQueryTableViewController's implementation of the method tableView:numberOfRowsInSection from the UITableViewDataSource. I've copy/pasted it from the GitHub repo containing PFQueryTableViewController.m

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSInteger count = [self.objects count];
    if ([self _shouldShowPaginationCell]) {
        count += 1;
    }
    return count;
}

It simply returns the count of objects to display (which makes sense), but if pagination is enabled, then it requires for an extra cell to be shown. This means you have to manually created another cell with the text "Load more data" or something like that, which would trigger a refresh.


A way to overcome this is simply by overriding tableView:numberOfRowsInSection yourself with the following:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.objects!.count
}

UPDATE 1

The prebuilt Parse pagination button was gone in previous answer


Use the following code snippet for calculating the height of the cells to display the prebuilt Parse pagination button

func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
    // Special case for pagination, using the pre-built one by Parse
    if (indexPath.row >= objects!.count) { return 50.0 }

    // Determines the height if an image ratio is present
    if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
        println("Ratio: \(ratio)")
        return tableView.bounds.size.width / CGFloat(ratio)
    } else {
        return 50.0
    }
}
0
R. Targa On

Using Parse 1.11 with iOS 9.2 and Xcode 7.2 Parse Pagination works perfectly. Problems surface when the user override some funcs used by Parse itself without properly managing the "Load More ..." row added by Parse. In my case I needed to override tableView-canEditRowAtIndexPath to determine whether the current user can or cannot delete the row according to the object's ACL. My initial func was:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {

    if let curUser = PFUser.currentUser() {
        let currentObject = objects![indexPath.row]
        if let acl = currentObject.ACL {
            return acl.getWriteAccessForUser(curUser)
        } else {
           return true
        }
    }
    return true
}

but I got the exception of indexpath out of bounds when the Load More line was met during list scrolling. Problem was solved adding this test:

    if (indexPath.row == self.objects!.count) { // row "Load More ..."
        return true
    }

Without this code the "Load More ..." row was not added by Parse!! So the complete correct overriding func is:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {

    if (indexPath.row == self.objects!.count) { // row "Load More ..."
        return true
    }
    if let curUser = PFUser.currentUser() {
        let currentObject = objects![indexPath.row]
        if let acl = currentObject.ACL {
            return acl.getWriteAccessForUser(curUser)
        } else {
           return true
        }
    }
    return true
}

Generally speaking all overridden funcs including heightForRowAtIndexpath, must take care of the extra line added by Parse when pagination is enabled.

HTH

Roberto Targa