Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why UITableView with autoresizing row height "scrolls up and then down" when loading more rows from remote server?

I have a UITableView with varying labels length in each row, so in my viewDidLoad I include this code to resize the table view row automatically according to its content:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100

With a fixed number of rows this is absolutely fine. But in my case the number of rows can become big at times, so I show 15 rows at a time to go a little quicker.

This is where I ask for more data from the remote server, in UITableViewDelegate method tableView(_:willDisplay:forRowAt:)

The problem is that when I insert a new set of rows, the table view kinda shakes up and down and a user would get lost where he/ she was in the table before new items were loaded.

The way I insert new rows into the table view is as follows:

func insertRowsInSection(_ tableView: UITableView, newObjects: [Object]) {
    var indexPaths = [IndexPath]()
    let lastRowPlusOne = tableView.numberOfRows(inSection: 0)

    if newObjects.count >= 1 {
        for objectIndex in 0...newObjects.count - 1 {
            let indexPath = IndexPath(row: objectIndex + lastRowPlusOne, section: 0)
            indexPaths.append(indexPath)
        }

        if #available(iOS 11.0, *) {
            tableView.performBatchUpdates({
                tableView.insertRows(at: indexPaths, with: .automatic)
            }, completion: nil)
        } else {
            tableView.beginUpdates()
            tableView.insertRows(at: indexPaths, with: .automatic)
            tableView.endUpdates()
        }
    }
}

I must say that when fixing the row height to a specific value this "shaking" behavior is not there at all.

like image 510
Ahmed Khedr Avatar asked Mar 03 '18 02:03

Ahmed Khedr


2 Answers

This is the issue of autosizing cell together with UITableView. UITableView will calculate it contentSize based on your estimatedHeight. Eg.

tableView.estimatedRowHeight = 100

this mean if you have 15 rows, your "estimated" tableView's contentSize will be around 1500px + (other possible like section header), but actual contentSize will be recalculated based on displayed cells after you scroll pass them. Then when you call

tableView.insertRows(at: indexPaths, with: .automatic)

it will perform animation to contentSize by using those estimatedSize (also your newly added cells will be calculated by estimatedSize) which mean it's not precise calculation which cause jumpy animation.

The best solution is.... go with manual calculate height with exact cell height return by UITableViewDelegate

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat

This will be a lot more work and break all purposes of autolayout with UITableViewCell. But it will give you very precise insert/delete/reload animation on UITableView. You still can use autolayout inside your cell but also include manual calculation for elements' margin or text size.

Another workaround is caching your cell height.

var cellHeights: [IndexPath: CGFloat] = [:]

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    guard let height = cellHeights[indexPath] else {
        return UITableViewAutomaticDimension
    }
    return height
}

func tableView(_ tableView: UITableView,  willDisplay cell: UITableViewCell,  forRowAt indexPath: IndexPath) {
    cellHeight[indexPath] = cell.frame.height
}

This will help you as workaround but it's the solution 100% solution for animation issue. I still go with manual height calculation if I want to use nice smooth tableView animation.

like image 168
tpeodndyy Avatar answered Sep 21 '22 23:09

tpeodndyy


Make sure you dispatch to main queue when calling your insertRowsInSection method and implement heightForRowAt indexPath based on the text length. Your label/s must have a fixed width, if only height should autoresize. e.g. assuming you have two labels stacked vertically with some padding in between:

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

    // get label1Text and label2Text from data-source 
    // get label widths and the font used in the labels
    let label1Height = label1Text.heightForFixed(width: label1Width, font: label1Font)
    let label2Height = label2Text.heightForFixed(width: label2Width, font: label2Font)

    return label1Height + label2Height + padding // the padding you want between the labels.
} 

Helper String extension to calculate bounding size for text length:

extension String {

    func heightForFixed(width: CGFloat, font: UIFont) -> CGFloat {
        let container = CGSize(width: width, height: .greatestFiniteMagnitude)
        let rect = self.boundingRect(with: container, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)

        return ceil(rect.height)
    }
}
like image 31
Lukas Avatar answered Sep 17 '22 23:09

Lukas