(Happy to accept an answer in Swift or Objective-C)
My table view has a few sections, and when a button is pressed, I want to insert a row at the end of section 0.
@GaétanZ's solution did not work for me (IOS12) but he's concept is right..
SO I did the next logical step:
IF table content does not know how tall is the cell THEN lets just “keep on scrolling" down RIGHT AFTER inserting the cell
private func insertBottomBubble(withCompletionHandler completion: (() -> Void)?) {
let bottomIndexPath = IndexPath(row: cbModelViewController!.viewModelsCount - 1, section: 0)
CATransaction.begin()
CATransaction.setAnimationDuration(0.9)
CATransaction.setCompletionBlock {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.scrollToBottom(withCompletionHandler: completion)
}
}
tableView.insertRows(at: [bottomIndexPath], with: isLeft == true ? .left : .right)
self.scrollToBottom(withCompletionHandler: nil) // no jump, keep it down :D
CATransaction.commit()
}
func scrollToBottom(withCompletionHandler completion: (() -> Void)?) {
let bottomMessageIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0) - 1, section: 0)
UIView.animate(withDuration: 0.45,
delay: TimeInterval(0),
options: UIView.AnimationOptions.curveEaseInOut,
animations: {
self.tableView.scrollToRow(at: bottomMessageIndexPath, at: .bottom, animated: false)
},
completion: { success in
if success {
completion?()
}
})
iOS 12 tested only
This problem occurs if the height of the cells varies greatly. Vlad’s solution works great. But difficult to implement. I suggest a simpler way. It will help you in most cases.
Add a variable private var cellHeightCache = [IndexPath: CGFloat]()
to your controller. And implement two delegate methods:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? 44
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
What everyone is saying about estimated row heights is true. So taking all of that into consideration here's the idea:
Store the heights of each row in a data structure (I choose a dictionary), then use that value from the dictionary for heightForRowAtIndexPath AND estimatedHeightForRowAtIndexPath methods
So the question is, how to get the row height if you are using dynamic label sizing. Well simple, just use the willDisplayCell method to find the cell frame
Here is my total working version, and sorry for the objective c...its just the project I'm working on right now:
declare a property for your dictionary:
@property (strong) NSMutableDictionary *dictionaryCellHeights;
init the dictionary:
self.dictionaryCellHeights = [[NSMutableDictionary alloc]init];
capture height:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [NSNumber numberWithDouble:cell.frame.size.height];
NSString *rowString = [NSString stringWithFormat:@"%d", indexPath.row];
[self.dictionaryCellHeights setObject:height forKey:rowString];
}
use height:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [self getRowHeight:indexPath.row];
if (height == nil){
return UITableViewAutomaticDimension;
}
return height.doubleValue;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [self getRowHeight:indexPath.row];
if (height == nil){
return UITableViewAutomaticDimension;
}
return height.doubleValue;
}
-(NSNumber*)getRowHeight: (int)row{
NSString *rowString = [NSString stringWithFormat:@"%d", row];
return [self.dictionaryCellHeights objectForKey:rowString];
}
Then when inserting the rows:
[self.tableViewTouchActivities performBatchUpdates:^{
[self.tableViewTouchActivities insertRowsAtIndexPaths:toInsertIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
} completion:^(BOOL finished){
[self.tableViewTouchActivities finishInfiniteScroll];
}];
*note - I'm using this library for infiniteScrolling https://github.com/pronebird/UIScrollView-InfiniteScroll/blob/master/README.md
I don't know how to fix it correctly, but my solution works for me
// hack: for fix jumping of tableView as for tableView difficult to calculate height of cells
tableView.hackAgainstJumping {
if oldIsFolded {
tableView.insertRows(at: indexPaths, with: .fade)
} else {
tableView.deleteRows(at: indexPaths, with: .fade)
}
}
extension UITableView {
func hackAgainstJumping(_ block: () -> Void) {
self.contentInset.bottom = 300
block()
self.contentInset.bottom = 0
}
}
for me work, turn off automatic estimate for tableview rows, section, headers and I use heightForRowAt
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row % 2 == 1 {
if arrowsVisible {
return 20
}
return 5
}
}
On iOS 11, UITableView uses estimated row height as default.
It leads to unpredictable behaviors when inserting/reloading or deleting rows because the UITableView has a wrong content size most of the time :
To avoid too many layout calculations, the tableView asks heightForRow
only for each cellForRow
call and remembers it (in normal mode, the tableView asks heightForRow
for all the indexPaths of the tableView). The rest of the cells has a height equal to the estimatedRowHeight
value until their corresponding cellForRow
is called .
// estimatedRowHeight mode
contentSize.height = numberOfRowsNotYetOnScreen * estimatedRowHeight + numberOfRowsDisplayedAtLeastOnce * heightOfRow
// normal mode
contentSize.height = heightOfRow * numberOfCells
One solution is to disable the estimatedRowHeight
mode by setting estimatedRowHeight to 0 and implementing heightForRow
for each of your cells.
Of course, if your cells have dynamic heights (with onerous layout calculations most of time so you used estimatedRowHeight
for a good reason), you would have to find a way to reproduce the estimatedRowHeight
optimization without compromising the contentSize of your tableView. Take a look at AsyncDisplayKit or UITableView-FDTemplateLayoutCell.
Another solution is to try to find a estimatedRowHeight
which suits well. Since iOS 10, you can also try to use UITableView.automaticDimension
. UIKit will find a value for you:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension
On iOS 11, it's already the default value.