treyhakanson treyhakanson - 3 months ago 11
Swift Question

Dynamically Resizing UITableViewCell Heights in iOS Swift

I am trying to create a UITableView whose cell's heights are sized according to the amount of text being displayed. This seemed simple enough, as

heightForRowAtIndexPath
is available and I can get the required size needed for the
UILabel
using the code shown below. However, this method is called before
cellForRowAtIndexPath
so I cannot size based on the content in the cell, which seems a very strange choice to me. What is the "best practice" way to dynamically size the height of the cells based on content?

In addition, why did Apple not have the
heightForRowAtIndexPath
method be called after
cellForRowAtIndexPath
?

I am using the following in
cellForRowAtIndexPath
to determine the required height for my label, but I'm not sure how to move this to
heightForRowAtIndexPath
since
dequeReusableCellWithIdentifier
cannot be called in the method (as far as I know):

func getRequiredLabelHeight(label: UILabel, text: String, font: UIFont) -> CGFloat {
label.numberOfLines = 0
label.lineBreakMode = .ByWordWrapping
label.font = font
label.text = text
label.sizeToFit()

return label.frame.height

}


I also know that I want the following to be my cell height, since I want the cell size to increase from the initial size (hinging on a 1 line label) based on the new label size (which can be any arbitrary length of lines)

self.cellHeight = getRequiredLabelHeight(commentCell.commentTextLabel, text: allComments![indexPath.row].text, font: UIFont(name: "Avenir", size: 16.0)!) + commentCell.bounds.height - commentCell.commentTextLabel.bounds.height


Thanks!

EDIT
My question does NOT pertain to resizing the label. The code I have provided does that. The issue is that the cell height is not constant; it varies throughout the table based on the content, so I can't use a static
tableview.rowHeight
. Since I cannot resize the cell height from
cellForRowAtIndexPath
, my content will not fit in the cell. For example, here is my
cellForRowAtIndexPath
(in a nutshell):

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let commentCell: CommentTableViewCell = tableView.dequeueReusableCellWithIdentifier("commentTableViewCell", forIndexPath: indexPath)
as! CommentTableViewCell

commentCell.commentTextLabel.textColor = Utils.hatchliTextDark
commentCell.commentTextLabel.font = UIFont(name: "Avenir", size: 16)

self.cellHeight = getRequiredLabelHeight(commentCell.commentTextLabel, text: allComments![indexPath.row].text, font: UIFont(name: "Avenir", size: 16.0)!) + commentCell.bounds.height - commentCell.commentTextLabel.bounds.height

commentCell.commentTextLabel.frame = CGRectMake(commentCell.commentTextLabel.frame.origin.x, commentCell.commentTextLabel.frame.origin.y, commentCell.commentTextLabel.frame.width, self.cellHeight)
commentCell.handleTextLabel.text = String(allComments![indexPath.row].creatorHandle)
commentCell.votesTextLabel.text = String(allComments![indexPath.row].votes)
commentCell.commentTextLabel.text = allComments![indexPath.row].text
commentCell.setMargin()

return commentCell

}


So, if the frame of the UILabel becomes larger
than tableview.rowHeight
, it will not appear. Thus, the row height needs to be resized on a per cell basis from
heightForRowAtIndexPath
(I assume)

Answer

SOLUTION

The solution I found is a little gnarly, but I don't see any other way. Basically, I know what proportion of the screen my UILabel's width will take up, in addition to having the String of text to be displayed and the font properties. Based on these factors, I can use the following code to determine what height of UILabel is needed, and by factoring in the default cell and label height can find my required cell height before the cell itself is generated:

// width of string as a particular font in one line
func getStringSizeForFont(font: UIFont, myText: String) -> CGSize {
    let fontAttributes = [NSFontAttributeName: font]
    let size = (myText as NSString).sizeWithAttributes(fontAttributes)

    return size

}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // get the string size
    let stringSizeAsText: CGSize = getStringSizeForFont(UIFont(name: "Avenir", size: 16.0)!, myText: allComments![indexPath.row].text)

    // label width and height are hardcoded here, but can easily be made dynamic based on the screen width
    // and the constant proportion of the width the UILabel will take up
    // example:
    // let labelWidth: CGFloat = UIScreen.mainScreen().bounds.width * widthProportion
    // originalLabelHeight can be hardcoded or made dynamic; I'm going to leave my hardcoded because I'll be leaving the height
    // the same across all devices

    let labelWidth: CGFloat = 259.0
    let originalLabelHeight: CGFloat = 20.5

    // the label can only hold its width worth of text, so we can get the ammount of lines from a specific string this way
    let labelLines: CGFloat = CGFloat(ceil(Float(stringSizeAsText.width/labelWidth)))

    // each line will approximately take up the original label's height
    let height =  tableView.rowHeight - originalLabelHeight + CGFloat(labelLines*stringSizeAsText.height)

    return height

}