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
How to Properly Animate Uiscrollview Contentoffset
How to Animate Uitableviewcell Height Using Auto-Layout
Titletextattributes Uiappearance Font in iOS 7
Clipstobounds Causes Uiimage to Not Display in iOS10 & Xcode 8
How to Dynamically Format a Number to Have Commas in a Uitextfield Entry
Perform UI Changes on Main Thread Using Dispatch_Async or Performselectoronmainthread
iOS 10 Rich Media Push Notification (Media Attachment) in Objective-C
Swift - Must Call a Designated Initializer of the Superclass Skspritenode Error
What Is the Impact of the "Requires Full Screen" Option in Xcode for an Iphone-Only App
Container View Getting Pushed Down as If It Had a Uinavigationbar
How Should a Swift + Objective-C Project Be Setup for Unit Testing
Change App Language in iOS Without Restarting the App
How to Retrieve Messages Logged with Os_Log from iPad/Iphone
Removing Object from Array in Swift 3
How to Use Mbprogresshud with Swift