18

My app has chat functionality and I'm feeding in new messages like this:

[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:messages.count - 1 inSection:1]] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:messages.count - 1 inSection:1] atScrollPosition:UITableViewScrollPositionBottom animated:YES];

However, my table view "jumps" weirdly when I'm adding a new message (either sending and receiving, result is the same in both):

enter image description here

Why am I getting this weird "jump"?

Can Poyrazoğlu
  • 33,241
  • 48
  • 191
  • 389
  • Have you figured it out? I'm seeing the same exact problem. – TotoroTotoro Mar 21 '16 at 19:27
  • @Macondo2Seattle I've ended up manually setting the height with the regular, pre-iOS 8 way of calculating heights. unfortunately, auto-height is buggy and broken with dynamic cell heights. – Can Poyrazoğlu Mar 21 '16 at 19:36
  • thanks! I think the jumping happens because when you scroll to the bottom, the table actually first scrolls to the top (!), and then to the bottom. I can't say the table does that. I also use dynamic cell heights. I'll see if I can still keep the dynamic heights -- it would suck to have to do it all manually! – TotoroTotoro Mar 21 '16 at 20:03
  • @Macondo2Seattle yes, it probably recalculates the height of every row again, which momentarily results in scrolling to top and then to the current row (which IS a bug IMO). Please let me know if you find any solution for that. – Can Poyrazoğlu Mar 21 '16 at 20:55
  • Can Poyrazoğlu, will do! – TotoroTotoro Mar 21 '16 at 20:58

5 Answers5

18

OK, I figured it out. As you say, the problem has to do with auto-sizing cells. I used two tricks to make things work (my code is in Swift, but it should be easy to translate back to ObjC):

1) Wait for the table animation to finish before taking further action. This can be done by enclosing the code that updates the table within a block between CATransaction.begin() and CATransaction.commit(). I set the completion block on CATransaction -- that code will run after the animation is finished.

2) Force the table view to render the cell before scrolling to the bottom. I do it by increasing the table's contentOffset by a small amount. That causes the newly inserted cell to get dequeued, and its height gets calculated. Once that scroll is done (I wait for it to finish using the method (1) above), I finally call tableView.scrollToRowAtIndexPath.

Here's the code:

override func viewDidLoad() {
    super.viewDidLoad()

    // Use auto-sizing for rows        
    tableView.estimatedRowHeight = 40
    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.dataSource = self
}

func chatManager(chatManager: ChatManager, didAddMessage message: ChatMessage) {
    messages.append(message)

    let indexPathToInsert = NSIndexPath(forRow: messages.count-1, inSection: 0)

    CATransaction.begin()
    CATransaction.setCompletionBlock({ () -> Void in
        // This block runs after the animations between CATransaction.begin
        // and CATransaction.commit are finished.
        self.scrollToLastMessage()
    })

    tableView.beginUpdates()
    tableView.insertRowsAtIndexPaths([indexPathToInsert], withRowAnimation: .Bottom)
    tableView.endUpdates()

    CATransaction.commit()
}

func scrollToLastMessage() {
    let bottomRow = tableView.numberOfRowsInSection(0) - 1

    let bottomMessageIndex = NSIndexPath(forRow: bottomRow, inSection: 0)

    guard messages.count > 0
        else { return }

    CATransaction.begin()
    CATransaction.setCompletionBlock({ () -> Void in

        // Now we can scroll to the last row!
        self.tableView.scrollToRowAtIndexPath(bottomMessageIndex, atScrollPosition: .Bottom, animated: true)
    })

    // scroll down by 1 point: this causes the newly added cell to be dequeued and rendered.
    let contentOffset = tableView.contentOffset.y
    let newContentOffset = CGPointMake(0, contentOffset + 1)
    tableView.setContentOffset(newContentOffset, animated: true)

    CATransaction.commit()
}
TotoroTotoro
  • 17,524
  • 4
  • 45
  • 76
  • I haven't actually tried it yet (because of some other stuff), but if it works, I'll accept it? It's really clever to wrap the update calls inside a CATransaction and wait for it to end to finish animation. It was probably two conflicting animations trying to run simultaneously, resulting in mess. – Can Poyrazoğlu Mar 22 '16 at 18:10
  • Nice to know. I'll try it when I have time :) – Can Poyrazoğlu Mar 22 '16 at 18:42
  • I do not think the insert view animation is really (or can be) used. Better use just `.none`? – Jonny May 22 '17 at 06:30
  • @Jonny: the animation may be necessary if the new row appears on the screen immediately. This can happen if you have few rows in your table to begin with. – TotoroTotoro May 22 '17 at 16:00
  • I used this for awhile. The performance seems to be really really slow if you have very many rows. I will try to find some way to optimize this. – Jonny Jun 09 '17 at 01:46
  • @Jonny that's a lot of rows! – TotoroTotoro Jun 09 '17 at 01:51
  • Probably worth profiling your app with Instruments to see what's slowing it down. – TotoroTotoro Jun 09 '17 at 01:52
  • 1
    I did. `setContentOffset` was slow... I changed to use `scrollRectToVisible` instead. Got better performance. It would just takes several seconds, up to 10, in scrollToLastMessage. – Jonny Jun 09 '17 at 09:48
  • `let height = CGFloat(10); tableview.scrollRectToVisible(CGRect.init(x: 0, y: tableview.contentSize.height - height, width: tableview.bounds.width, height: height), animated: animated);` – Jonny Jun 09 '17 at 09:52
  • Very interesting. Thanks for the tip! – TotoroTotoro Jun 09 '17 at 17:50
1

Change UITableViewRowAnimationBottom to UITableViewRowAnimationNone and try

Arun Kumar P
  • 820
  • 2
  • 12
  • 25
  • I have created a sample project for this and it is working fine. I think you have done any other process in main thread during the new row insertion. So it will make slow UI updates. so please comment the other task and try it. if it working good then please use thread or async function for other operations. – Arun Kumar P Feb 21 '16 at 10:18
  • nope. can you edit your answer to include the exact code (inserting rows part) that you succeed with? – Can Poyrazoğlu Feb 21 '16 at 10:19
  • The `data` is just datasource of the table ` [self.tableView beginUpdates]; [data addObject:@"dummy data"]; [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:data.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade]; [self.tableView endUpdates]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:data.count - 1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];`. – Arun Kumar P Feb 21 '16 at 10:28
  • This exact code causes exactly what I've posted in the question. – Can Poyrazoğlu Feb 21 '16 at 10:35
  • `- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"reuseIdentifier" forIndexPath:indexPath]; // Configure the cell... cell.textLabel.backgroundColor = [UIColor blueColor]; return cell; } ` cross check the cell fro row at index path. – Arun Kumar P Feb 21 '16 at 10:55
0

Try This!

UITableViewRowAnimation rowAnimation = UITableViewRowAnimationTop;
UITableViewScrollPosition scrollPosition = UITableViewScrollPositionTop;

[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:rowAnimation];
[self.tableView endUpdates];

[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES];

// Fixes the cell from blinking (because of the transform, when using translucent cells)
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
rushisangani
  • 3,195
  • 2
  • 14
  • 18
0

For Swift 3 and 4

for scroll down to bottom of table View automatically when add new item in the table view just in tableView function add following line its works me

tableView.scrollToRow(at: IndexPath, at: .bottom, animated: true)

for example in my case I have only one section so 0 is use for section and I have list of orderItems so for last index I use orderItems.count - 1

tableView.scrollToRow(at: [0, orderItems.count - 1], at: .bottom, animated: true)

Syed Hasnain
  • 130
  • 1
  • 8
-1

I've just found out that on ios 11 this problem no longer exists. So there's no longer a content jump when adding a row to a table view and then scrolling to it with scrollToRow(at:) .

Also, on ios 10 calling scrollToRowAtIndexPath with animated=false fixes the content jump

andrei
  • 1,353
  • 15
  • 24