Uitableview Scrolls to Top When Reloading Cells with Changing Cell Heights

UITableView scrolls to top when reloading cells with changing cell heights

Always update the UI on the main thread. So just place

[self.tableView reloadData];

inside a main thread:

dispatch_async(dispatch_get_main_queue(), ^{
//UI Updating code here.
[self.tableView reloadData];
});

UITableView with dynamic cell heights jumping when scrolling up after reloading cell

This behavior appears to be a bug, if for no other reason than it's no longer reproducible on iOS 9. I'm sure that's not much consolation.

The issue primarily derives from having an inaccurate estimate, like @NickCatib said. The best you can do on iOS 8 is to improve the estimation. A technique many have recommended is to cache heights in willDisplayCell and use them on subsequent calls to estimatedRowHeightAtIndexPath.

You might be able to mitigate the behavior by not doing anything to get UITableView to discard its caches, like by modifying the content in a cell directly using cellForRowAtIndexPath rather than using reloading if it's onscreen. However, that won't help if you actually need to change the height of the cell.

I'm afraid to say the bug can't be easily be fixed within a table view, as you don't have control over the layout. The bug can be more easily worked around in a subclass UICollectionViewFlowLayout by changing the contentOffsetAdjustment during invalidation, although that might not be terribly easy.

reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling

To prevent jumping you should save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:

Swift:

var cellHeights = [IndexPath: CGFloat]()

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

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}

Objective C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
if (height) return height.doubleValue;
return UITableViewAutomaticDimension;
}

TableView w. Dynamic Height scrolls when reloading with new data

The approach that ended up working smoothly was:

Step 1. Storing in a variable the UITableViewCell below the Navigation Bar and storing the offset of that cell in relation to the Navigation Bar.

Step 2. Insert cells / reloadData.

Step 3.Scroll to the cell we saved before the insert/reload and then add the offset.

Here’s the code:

UIView.performWithoutAnimation {
//Step 1 Detect & Store
let topWillBeAt = getTopVisibleRow() + countOfAddedItems
let oldHeightDifferenceBetweenTopRowAndNavBar = heightDifferenceBetweenTopRowAndNavBar()

//Step 2 Insert
self.tableView.insertRows(at: arrayOfIndexPaths, with: .none)

//Step 3 Restore Scrolling
tableView.scrollToRow(at: IndexPath(row: topWillBeAt, section: 0), at: .top, animated: false)
tableView.contentOffset.y = tableView.contentOffset.y - oldHeightDifferenceBetweenTopRowAndNavBar

}

And supporting functions:

  func getTopVisibleRow () -> Int {
//We need this to accounts for the translucency below the nav bar
let navBar = navigationController?.navigationBar
let whereIsNavBarInTableView = tableView.convert(navBar!.bounds, from: navBar)
let pointWhereNavBarEnds = CGPoint(x: 0, y: whereIsNavBarInTableView.origin.y + whereIsNavBarInTableView.size.height + 1)
let accurateIndexPath = tableView.indexPathForRow(at: pointWhereNavBarEnds)
return accurateIndexPath?.row ?? 0
}

func heightDifferenceBetweenTopRowAndNavBar()-> CGFloat{
let rectForTopRow = tableView.rectForRow(at:IndexPath(row: getTopVisibleRow(), section: 0))
let navBar = navigationController?.navigationBar
let whereIsNavBarInTableView = tableView.convert(navBar!.bounds, from: navBar)
let pointWhereNavBarEnds = CGPoint(x: 0, y: whereIsNavBarInTableView.origin.y + whereIsNavBarInTableView.size.height)
let differenceBetweenTopRowAndNavBar = rectForTopRow.origin.y - pointWhereNavBarEnds.y
return differenceBetweenTopRowAndNavBar

}

UITableView changes scroll position when cell height changes

Just reload cell not entire tableview, like bellow:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 
self.cellsMode[indexPath.row] = !self.cellsMode[indexPath.row]
tableView.reloadRows(at: [indexPath], with: .automatic)
}

UITableView dynamic cell heights only correct after some scrolling

I don't know this is clearly documented or not, but adding [cell layoutIfNeeded] before returning cell solves your problem.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
NSUInteger n1 = firstLabelWordCount[indexPath.row];
NSUInteger n2 = secondLabelWordCount[indexPath.row];
[cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];

[cell layoutIfNeeded]; // <- added

return cell;
}

UITableview Scrolls to Top on Reload

Igor's answer is correct if you are using dynamically resizable cells (UITableViewAutomaticDimension)

Here it is in swift 3:

    private var cellHeights: [IndexPath: CGFloat?] = [:]
var expandedIndexPaths: [IndexPath] = []

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

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeights[indexPath] {
return height ?? UITableViewAutomaticDimension
}
return UITableViewAutomaticDimension
}

func expandCell(cell: UITableViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
if !expandedIndexPaths.contains(indexPath) {
expandedIndexPaths.append(indexPath)
cellHeights[indexPath] = nil
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
//tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
}


Related Topics



Leave a reply



Submit