Search code examples
iosswiftparse-platformpfquerytableviewcontrolle

Dynamic cell height in PFQueryTableViewController with images


Simple: I want the size of the picture with width of the screen and the height calculated to keep the aspect ratio, similar to 9gag app.

How can I make dynamic height of cell in PFQueryTableViewController, in my cell I have one label and PFImageView in a Custom cell and loading works fine, but the pictures are not changing the height of the cell and all of the cells have the same height. I am stuck on this problem for third day and it looks like error from the parse.com framework. I was able to change the cell height when I was working with UITableViewController but the parse framework ignores it.

I am using storyboard and tried that with autolayout constraint and without it. TableViewController.swift

import UIKit
import Parse
import ParseUI
import Bolts
class TableViewController: PFQueryTableViewController {

// 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.pullToRefreshEnabled = true
    self.paginationEnabled = false

}

// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery {

    // Start the query object
    var query = PFQuery(className: "Image")
    query.whereKey("deleted", equalTo: 0)
    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("CustomCell") as! CustomTableViewCell!
    if cell == nil {
        cell = CustomTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "CustomCell")
    }

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

    // display initial image
    var initialThumbnail = UIImage(named: "question")
    cell.postImageView.image = initialThumbnail
    // extract image
    if let thumbnail = object?["image"] as? PFFile {
        cell.postImageView.file = thumbnail
        cell.postImageView.loadInBackground()
    }
    cell.postImageView.contentMode = UIViewContentMode.ScaleAspectFit
    cell.postImageView.clipsToBounds = true

    cell.actualWidth = cell.postImageView.image!.size.width
    cell.actualHeight = cell.postImageView.image!.size.height

    return cell
}


    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        var cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomTableViewCell!
        let aspect = cell.actualWidth / cell.actualHeight
        var height: CGFloat = cell.postImageView!.frame.size.width * aspect
        return height
    }
}

CustomTableViewCell.swift

import UIKit
import Parse
import ParseUI
class CustomTableViewCell: PFTableViewCell {
 var actualHeight: CGFloat = 10
 var actualWidth: CGFloat = 10
 @IBOutlet weak var postHeadlineLabel: UILabel!
 @IBOutlet weak var postImageView: PFImageView!
}

I found some advices online but it doesnt work and seems like error of the parse framework with iOS8 I tried

cell.postImageView.contentMode = UIViewContentMode.ScaleAspectFit
cell.postImageView.clipsToBounds = true
cell.sendSubviewToBack(cell.postImageView)

or

override func awakeFromNib() {
    self.setTranslatesAutoresizingMaskIntoConstraints(false)
}

Update - parse DB: https://www.anony.ws/image/D6tp


Solution

  • PFQueryTableViewController inherits from UITableViewController, which means it conforms to the UITableViewDelegate protocol. Inside here there is a method that you can override called:

    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
    

    In here you should return a specific CGFloat (for example 50.0f for 50px or something else) depending on the size of that specific cell given by the indexPath.


    UPDATE 1:

    OP provided more code to elaborate the issue


    Since you are calling tableView.dequeueReusableCellWithIdentifier in the tableView:heightForRowAtIndexPath, then it means that once you've got one cell with an image ready, then it'll keep using this aspect ratio over and over. There is no logic in your tableView.dequeueReusableCellWithIdentifier which is tied to the unique object at the provided indexPath.

    One way to overcome this could be by calling objectAtIndexPath(indexPath), which is a method from PFQueryTableViewController. This will give you access to the PFObject and PFFile at the specified cell. The downside to this is that you'll have to download the image again, thus using a lot of band-width. It will also slow down your app since you'll have to wait for it to download.

    You could also make use of tableView.cellForRowAtIndexPath(indexPath) to get something unique for that cell, but unfortunately tableView:heightForRowAtIndexPath is called before tableView:cellForRowAtIndexPath in the PFQueryTableViewController which will cause the app to crash.


    My best suggestion is to add another field to the database for your Image class which stores the aspect ratio of the image.

    If this is not possible since you already have a large database or some other good reason, then I would recommend for you to make a cloud function which can compute the aspect ratio.


    UPDATE 2

    Updated solution to answer


    After sitting and playing around with it for a while, then I managed to come up with a solution for your issue. Unfortunately it is not pretty and I will still suggest you to store the aspect ratio in your DB schema as the most optimal solution.

    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return calculateHeightForRowAtIndexPath(indexPath);
    }
    
    func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
        if let file = objectAtIndexPath(indexPath)?["image"] as? PFFile {
            let image = UIImage(data: file.getData()!)
            let imageToScreenRatio = tableView.bounds.size.width / (image?.size.width)!
            return imageToScreenRatio * (image?.size.height)!
        }
        else {
            return 50.0
        }
    }
    

    The reason why this is really bad is because the image is being downloaded in the main UI thread using file.getData(). Instead file.getDataInBackgroundWithBlock(block) should be used, but this would cause problems since calculateHeightForRowAtIndexPath would then return before having downloaded the image.


    UPDATE 3

    OP provided more info on DB schema


    Since you already have the ratio stored in the database, then you can calculate the cell height the following way:

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