iosswiftuitableviewheightforrowatindexpath

Dynamically change UITableViewCell height - Swift 4


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
        }
    }
}

Solution

  • 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.