14

How can one insert rows into a UITableView at the very top without causing the rest of the cells to be pushed down - so the scroll position does not appear to change at all?

Just like Facebook and Twitter when new posts are delivered, they are inserted at the top but the scroll position remains fixed.

My question is similar to this this question. What makes my question unique from that question is that I'm not using a table with fixed row heights - I'm using UITableViewAutomaticDimension and an estimatedRowHeight. Therefore the answers suggested there will not work because I cannot determine the row height.

I have tried this solution that doesn't involve taking row height into consideration, but the contentSize is still not correct after reloading, because the contentOffset set isn't the same relative position - the cells are still pushed down past where they were before the insert. This is because the cell hasn't been rendered on screen so iOS doesn't bother to calculate the appropriate height for it until it's about to appear, therefore contentSize is not accurate.

CGSize beforeContentSize = tableView.contentSize;
[tableView reloadData];
CGSize afterContentSize = tableView.contentSize;
CGPoint afterContentOffset = tableView.contentOffset;
tableView.contentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);

Alain pointed out rectForCellAtIndexPath which forces iOS to calculate the appropriate height. I can now determine the proper height for the inserted cells, but the scroll view's contentSize is still not correct, as is evidenced when I iterate over all cells and add up the heights which is a larger than contentSize.height. So ultimately when I set the contentOffset manually it's not scrolling to the correct location.

Original code:

//update data source
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:newIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];

In this scenario, what can be done to achieve the desired behavior?

Community
  • 1
  • 1
Jordan H
  • 52,571
  • 37
  • 201
  • 351

5 Answers5

11

Late to the party but this works even when cell have dynamic heights (a.k.a. UITableViewAutomaticDimension), no need to iterate over cells to calculate their size, but works only when items are added at the very beginning of the tableView and there is no header, with a little bit of math it's probably possible to adapt this to every situation:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row == 0 {
            self.getMoreMessages()
        }
}

private func getMoreMessages(){
        var initialOffset = self.tableView.contentOffset.y
        self.tableView.reloadData()
        //@numberOfCellsAdded: number of items added at top of the table 
        self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: numberOfCellsAdded, inSection: 0), atScrollPosition: .Top, animated: false)
        self.tableView.contentOffset.y += initialOffset
}
Azephiar
  • 501
  • 6
  • 19
  • 2
    This is the only solution that could help me of the very many that are already out there! There is one problem though: the inertial scrolling is immediately terminated. Is there a way to keep the current scrolling speed? – Nickkk Jan 08 '17 at 14:28
  • I' ve not been able to solve this yet, if you find a solution please let me know – Azephiar Jan 11 '17 at 12:03
  • This is by far the best solution I found for this problems after weeks of working on this. – manueGE Jan 11 '23 at 20:00
1

Also, single row sections can accomplish desired effect

If a section already exists at the specified index location, it is moved down one index location. https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/index.html#//apple_ref/occ/instm/UITableView/insertSections:withRowAnimation:

let idxSet = NSIndexSet(index: 0)
self.verseTable.insertSections(idxSet, withRowAnimation: .Fade)
Loaf Box
  • 11
  • 1
0

I think your solution was almost there. You should have based the new Y offset on the "before" Y offset rather than the "after".

..., beforeContentOffset.y + afterContentSize.height - beforeContentSize.height

(you would need to save the beforeContentOffset point before the reload data though)

Alain T.
  • 40,517
  • 4
  • 31
  • 51
  • This didn't do the trick. It pushes it down still likely due to complexities in calculating the correct content size when using dynamic row heights. – Jordan H Jan 12 '16 at 05:11
  • Perhaps this would work better with your original code. My feeling is that you would need to find an event that is triggered after the cell rendering/calculations are complete to reposition the offset such as didEndDisplayingCell for example or on a timer.. – Alain T. Jan 12 '16 at 05:42
  • This would result in a flash. If I insert a cell it will immediately insert it and reposition the cells, then I set the content offset after some time to jump back to where they were, it will be quite noticeable. – Jordan H Jan 12 '16 at 05:46
  • Agreed, but making it position properly could be a starting point from which you can work backwards to find a better place to trigger the offset adjustment knowing that the calculation is correct. – Alain T. Jan 12 '16 at 06:02
  • Here's another idea: Perhaps the rectForRowAtIndexPath method can give the appropriate offset delta to use (intuitively it should be the first to know the real cell height). Given that contentSize is lower level and may not have had a chance to be fully recalculated until a full rendering cycle is completed, it may be unusable to establish the difference in this case. – Alain T. Jan 12 '16 at 07:17
  • Looks like `rectForRowAtIndexPath` does return the correct height, but setting the content offset is modifying the scroll position - not scrolled down enough. The content offset and content size is likely not fully calculated. I tried setting content offset to `CGPointMake(afterContentOffset.x, beforeContentOffset.y + heightOfNewRows)` and `CGPointMake(afterContentOffset.x, afterContentOffset.y - heightOfNewRows)` – Jordan H Jan 12 '16 at 21:39
  • 1
    I can confirm `contentSize` is not correct after reloading. When I iterate over all cells and add up the heights using `rectForRowAtIndexPath` I get a larger number than `contentSize.height`. – Jordan H Jan 12 '16 at 21:51
0

Meet the same problem and still do not find a elegant way to solve it.Here is my way,suppose I want to insert moreData.count cells above the current cell.

// insert the new data to self.data
for (NSInteger i = moreData.count-1; i >= 0; i--) {
    [self.data insertObject:moreData[i] atIndex:0];
}

//reload data or insert row at indexPaths
[self.tableView reloadData];

//after 0.1s invoke scrollToRowAtIndexPath:atScrollPosition:animated:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:moreData.count inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
});

The advantage is fixed the position before and after inserting new data.The disadvantage is we can see the screen flash (the topmost cell shows and move up)

Owen
  • 9
  • 3
0

My working solution for iOS 13 & Swift 5:

Note: Only tested with dynamic cell heights (UITableViewAutomaticDimension / UITableView. automaticDimension).

P.S. None of the solutions posted here did work for me.

extension UITableView {
    /**
     Method to use whenever new items should be inserted at the top of the table view.
     The table view maintains its scroll position using this method.
     - warning: Make sure your data model contains the correct count of items before invoking this method.
     - parameter itemCount: The count of items that should be added at the top of the table view.
     - note: Works with `UITableViewAutomaticDimension`.
     - links: https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/
     */
    func insertItemsAtTopWithFixedPosition(_ itemCount: Int) {
        layoutIfNeeded() // makes sure layout is set correctly.
        var initialContentOffSet = contentOffset.y

        // If offset is less than 0 due to refresh up gesture, assume 0.
        if initialContentOffSet < 0 {
            initialContentOffSet = 0
        }

        // Reload, scroll and set offset:
        reloadData()
        scrollToRow(
            at: IndexPath(row: itemCount, section: 0),
            at: .top,
            animated: false)
        contentOffset.y += initialContentOffSet
    }
}
Baran
  • 2,710
  • 1
  • 23
  • 23