Dynamically change UITableViewCell height - Swift 4

2.5k views Asked by At

How do I go about dynamically changing the UITableViewCell height? I've tried implementing the following, but for some reason, it isn't working. The code crashes as soon as I load the view controller displaying this table view

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let cell = tableView.cellForRow(at: indexPath) as! AvailableRideCell
    return cell.getHeight()
}

This is the getHeight function in my AvailableRideCell

func getHeight() -> CGFloat {
    return self.destinationLabel.optimalHeight + 8 + self.originLabel.optimalHeight + 8 + self.priceLabel.optimalHeight
}

And this is the optimalHeight function

extension UILabel {
    var optimalHeight : CGFloat {
        get {
            let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
            label.numberOfLines = 0
            label.lineBreakMode = NSLineBreakMode.byWordWrapping
            label.font = self.font
            label.text = self.text
            label.sizeToFit()
            return label.frame.height
        }
    }
}
3

There are 3 answers

12
Yury Imashev On BEST ANSWER

You code crashes because of this line

let cell = tableView.cellForRow(at: indexPath) as! AvailableRideCell

From Apple Documentation we know that this method is used for optimization. The whole idea is to get cells heights without wasting time to create the cells itself. So this method called before initializing any cell, and tableView.cellForRow(at: indexPath) returns nil. Because there are no any cells yet. But you're making force unwrapping with as! AvailableRideCell and your code crashed.

So at first, you need to understand, why you should not use any cells inside the cellForRow(at ) method. After that, you need to change the logic so you could compute content height without calling a cell.

For example, in my projects, I've used this solution

String implementation

extension String {
    func height(for width: CGFloat, font: UIFont) -> CGFloat {
        let maxSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
        let actualSize = self.boundingRect(with: maxSize,
                                           options: [.usesLineFragmentOrigin],
                                           attributes: [NSAttributedStringKey.font: font],
                                           context: nil)
        return actualSize.height
    }
}

UILabel implementation

extension String {
    func height(for width: CGFloat, font: UIFont) -> CGFloat {
        let labelFrame = CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
        let label = UILabel(frame: labelFrame)
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        label.font = font
        label.text = self
        label.sizeToFit()
        return label.frame.height
    }
}

With that, all you need to do is to compute your label and store its font.

var titles = ["dog", "cat", "cow"]

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // number of rows is equal to the count of elements in array
    return titles.count
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellTitle = titles[indexPath.row]
    return cellTitle.height(forWidth: labelWidth, font: labelFont)
}

Dynamic rows height changing

If you'll need to update row height, all you need to do is to call this method when your content had been changed. indexPath is the index path of changed item.

tableView.reloadRows(at: [indexPath], with: .automatic)

Hope it helps you.

2
Alexis O On

Keep in mind that UITableViewCell is reused. So getting the height of the current cell can be unstable.

A better way is to have one fake/placeholder cell (I call the calculator cell) and use that to calculate the size of the cell.

So in the heightForRowAt method, you get the data instead of the cell. Put that data inside the calculator cell and get the height from there.

0
perec On

You don't mention if you are using Auto Layout, but if you are, you can let Auto Layout manage the height of each row. You don't need to implement heightForRow, instead set:

tableView.rowHeight = UITableViewAutomaticDimension

And configure your UILabel with constraints that pin it to the cell's content view:

    let margins = contentView.layoutMarginsGuide
    NSLayoutConstraint.activate([
        cellLabel.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
        cellLabel.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
        cellLabel.topAnchor.constraint(equalTo: margins.topAnchor),
        cellLabel.bottomAnchor.constraint(equalTo: margins.bottomAnchor)
    ])

Each row will expand or contract to fit the label's intrinsic content size. The height is automatically adjusted if the device landscape/portrait orientation changes, without re-loading the cell. If you want the row height to change automatically when the device's UIContentSizeCategory changes, set the following:

cellLabel.adjustsFontForContentSizeCategory = true