66
CGPoint offset = [_table contentOffset];
[_table reloadData];
[_table setContentOffset:offset animated:NO];    //unuseful

//    __block UITableView *tableBlock = _table;
//    [self performBlock:^(id sender) {
//        [tableBlock setContentOffset:offset];
//    } afterDelay:2];

I know don't know of any delegate method which gets called after reloadData. And using afterDelay:2 which is kind of a hack may be too short or too long, so how can I implement it?

mfaani
  • 33,269
  • 19
  • 164
  • 293
avincross
  • 663
  • 1
  • 6
  • 5
  • See [here](https://stackoverflow.com/a/39648633/5175709). This answer works better than all answers provided here. Even Matt Koala's answer my not work 100% of the time :) – mfaani Oct 10 '18 at 04:05

13 Answers13

122

I was having trouble with this because I mess with cell sizing in my cellForRowAtIndexPath method. I noticed that the sizing information was off after doing reloadData, so I realized I needed to force it to layout immediately before setting the content offset back.

CGPoint offset = tableView.contentOffset;
[tableView.messageTable reloadData];
[tableView layoutIfNeeded]; // Force layout so things are updated before resetting the contentOffset.
[tableView setContentOffset:offset];
Matt Koala
  • 2,171
  • 2
  • 18
  • 14
  • 27
    I don't understand the accepted answer. I see reloadData definitely shifting my content offset and this helped it. – olivaresF Jul 31 '15 at 03:56
  • layoutIfNeeded() itself worked for me after reloadData() (Swift 3.0) – Engnyl Sep 29 '16 at 14:13
  • 3
    `[tableView layoutIfNeeded];` solved my problem. Thank you! – Andrew Bogaevskyi May 19 '17 at 12:30
  • 6
    For those of you using `UITableViewAutomaticDimension` in `tableView: heightForRowAtIndexPath:`, in order to get this to work under iOS 10 I also had to return ``UITableViewAutomaticDimension` in `tableView: estimatedHeightForRowAtIndexPath:` to get it to work. Returning a constant in this method didn't work for me. – Evan R Aug 09 '17 at 21:16
  • The tableView gets stuck lower on the screen if it is reloaded while scrolling using this solution – vikzilla Feb 02 '18 at 06:07
  • What if you have 100 rows and are looking at the last row before reloading, then after reloading just 1 row - it doesn't make sense to keep the original offset. – Jonny Aug 08 '18 at 01:48
  • @Jonny is the user looking at the last row? If so then yes it does make sense to keep his original offset! – mfaani Oct 10 '18 at 03:46
  • just make sure you don't call this inside an animation block. The result would be awkward :/ – mfaani Nov 14 '18 at 17:56
  • Did NOT work, It fixes the problem on iOS 12 but it doesn't on iOS13 – Bigair May 23 '20 at 14:14
97

Calling reloadData on the tableView does not change the content offset. However, if you are using UITableViewAutomaticDimension which was introduced in iOS 8, you could have an issue.

While using UITableViewAutomaticDimension, one needs to write the delegate method tableView: estimatedHeightForRowAtIndexPath: and return UITableViewAutomaticDimension along with tableView: heightForRowAtIndexPath: which also returns the same.

For me, I had issues in iOS 8 while using this. It was because the method estimatedHeightForRowAtIndexPath: method was returning inaccurate values even though I was using UITableViewAutomaticDimension. It was problem with iOS 8 as there was no issue with iOS 9 devices.

I solved this problem by using a dictionary to store the value of the cell's height and returning it. This is what I did.

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSNumber *key = @(indexPath.row);
    NSNumber *height = @(cell.frame.size.height);

    [self.cellHeightsDictionary setObject:height forKey:key];
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSNumber *key = @(indexPath.row);
    NSNumber *height = [self.cellHeightsDictionary objectForKey:key];

    if (height)
    {
        return height.doubleValue;
    }

    return UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

The check for whether height exists is for the first time page loads.

Skywalker
  • 1,590
  • 1
  • 18
  • 36
  • 5
    But I have one improvement! Save the indexPath itself instead of just the row. Since I have multiple sections this was a problem... – blackjacx May 31 '16 at 17:23
  • @blackjacx Yes. If you have multiple sections, your suggestion would be helpful. Thanks for that – Skywalker Jun 01 '16 at 14:25
  • 4
    This answer works. However, if your cells contain items that can change height dynamically, like a UITextView, then you need to change the stored height for the row when the height of the cell changes. I update the height when the textViewDidChange() delegate method is called. Works like a charm. – E.Freitas Oct 14 '16 at 18:40
  • 3
    This is the CORRECT ANSWER for people using Auto Layout. In 'viewDidLoad', you need to set a non-zero value for 'self.tableView.estimatedRowHeight', and then call 'self.tableView.rowHeight = UITableViewAutomaticDimension'. Then, you need to override 'tableView(:heightForRowAt:)' to return 'UITableViewAutomaticDimension', and also override tableView(:estimatedHeightForRowAt:) to do the same. What an API, huh? Stellar. – Womble Jul 09 '17 at 23:47
  • Thanks , this should be set to be the accept answer , set estimatedRowHeight to 0 solved my problem of contentOffset changing when tableView reloadData – ximmyxiao Nov 30 '17 at 02:39
  • This could be an accepted answer depending on the circumstances. This worked for MY project more than any of the other answers. My pagination table view was jumping all over the place when loading new cells. This solved my problem. – Michael Gaines Mar 16 '18 at 20:27
  • This answer works for me, while the accepted answer doesn't. – tounaobun Jul 09 '19 at 10:12
49

Swift 5 variant of @Skywalker answer:

private var heightDictionary: [IndexPath: CGFloat] = [:]

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

public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let height = heightDictionary[indexPath]
    return height ?? UITableView.automaticDimension
}

Another solution (fetched from MessageKit):

This method should be called instead of reloadData. This can fit for specific cases.

extension UITableView {
    public func reloadDataAndKeepOffset() {
        // stop scrolling
        setContentOffset(contentOffset, animated: false)
            
        // calculate the offset and reloadData
        let beforeContentSize = contentSize
        reloadData()
        layoutIfNeeded()
        let afterContentSize = contentSize
            
        // reset the contentOffset after data is updated
        let newOffset = CGPoint(
            x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
            y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
        setContentOffset(newOffset, animated: false)
    }
}
Okhan Okbay
  • 1,374
  • 12
  • 26
Nike Kov
  • 12,630
  • 8
  • 75
  • 122
29

In my case uncheck row height automatic and estimate automatic problem solved

fix reload problem

saranpol
  • 2,177
  • 1
  • 23
  • 22
28

By default, reloadData keeps the contentOffset. However, it could be updated if you do have inaccurate estimatedRowHeight values.

Cyril
  • 361
  • 5
  • 5
  • 1
    How to set estimatedRowHeight correctly if the height of cell is dynamic for each? – Nike Kov Mar 06 '18 at 09:16
  • @NikKov, as the name suggest it, it is an estimated value that doesn't need to be the exact actual size of your cell. If you know your cells height are generally going to be between 40 and 60 points, then set 50 as estimatedRowHeight. – Cyril Mar 07 '18 at 09:44
  • 2
    @Cyril you said `it is an estimated value that doesn't need to be the exact actual size of your cell`, but also said `if you do have inaccurate estimatedRowHeight values`. So how exactly do we know which is accurate? E.g. my cells usually at 40 and 60 and I set 50. However, sometimes an unsual data pop out cause the cell's height grow up to 120, may be 240 or 300. Then what should the accurate estimatedRowHeight should be? – Eddie May 08 '18 at 08:33
  • 1
    Hi @Eddie, you can put a couple of `if` `else` statement in `estimatedRowHeight` since it doesn't take much computing time. Therefore, I would suggest you to return a different estimated row height for the unusual data. 120 to 300 is big range, if you have no efficient way to know an approximate size then you should return `UITableViewAutomaticDimension`. – Cyril May 10 '18 at 13:02
13

If you implement estimatedHeightForRowAtIndexPath method and your estimate is not right, you will possible get into this situation.

To solve this, you can return a large height that bigger than every cell height in your tableView, like this:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { 
    return 800.f; // if 800 is bigger than every possible value of your cell height.
}
bingxin xue
  • 139
  • 1
  • 4
  • Just a note, I had this issue and due to the change in content offset, I was able to see a blank space. The solution here might be working good. But, for me While the reload the cellForRowAtIndexPath for 0,0 is not getting called. I had to fix this by returning CGFloat.leastNonzeroMagnitude to estimatedRowHeight. – Sujananth Mar 16 '20 at 12:33
10

If you insert data at the beginning of your dataSource array, you need to change contentOffset like this: Swift 3+

func prepareReloadData() {
    let previousContentHeight = tableView.contentSize.height
    let previousContentOffset = tableView.contentOffset.y
    tableView.reloadData()
    let currentContentOffset = tableView.contentSize.height - previousContentHeight + previousContentOffset
    tableView.contentOffset = CGPoint(x: 0, y: currentContentOffset)
}
Bohdan Savych
  • 3,310
  • 4
  • 28
  • 47
  • 1
    You need to place `tableView.layoutIfNeeded()` after `tableView.reloadData()`. This will give you the updated `currentContentHeight`. – Alan Scarpa Jan 02 '18 at 18:15
9

I was recently working with reloadData -- reloadData doesn't change the contentOffset or scroll the table view. It actually stays the same if the offset is less than the new amount of data.

Roy
  • 3,574
  • 2
  • 29
  • 39
2

I had the same issue however none of answers suggested here worked. Here's how i solved it. Subclass UITableView and override layoutSubviews method like this:

override func layoutSubviews() {
    let offset = contentOffset
    super.layoutSubviews()
    contentOffset = offset
}
Mikhail Vasilev
  • 1,147
  • 9
  • 14
  • However: this does not seem to work if you are animating between two layouts, unfortunately. – Fattie Sep 26 '19 at 01:53
2

@Skywalker's answer showed best workaround for estimated height of cells problem. But sometimes problem lyes in a different place.
Sometimes the problem lyes in contentInsets of table view. If you make reload data while tableView is not visible on the screen you can face with wrong offset after the table view appears on the screen.
It happens because UIViewController can control insets if his scrollView when the scrollView is appearing to allow lying of scrollView below transparent navigationBar and statusBar.
I've faced with this behaviour in iOS 9.1

Andrew Romanov
  • 4,774
  • 3
  • 25
  • 40
2

Matt's answer above made me realize that it has to be an issue with the estimatedRowHeight.

So as several pointed out reloadData should not modify the contentOffset so when you set your rowHeight = UITableView.automaticDimension just to be sure to set a correct estimatedRowHeight.

If the estimatedRowHeight is shorter than then UITableView estimated it will try to fix the height and the scrolling behavior would appear but only if the cell with the height problem is visible. In other words, be sure your estimatedRowHeight is set correctly.

I hope this could help someone else.

Victor Sigler
  • 23,243
  • 14
  • 88
  • 105
-3

This is working 100%

change the tableView.reloadData() 

into

tableView.reloadRows(at: tableView!.indexPathsForVisibleRows!, with: .none)
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
-4

For it works fine

[tView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
             atScrollPosition:UITableViewScrollPositionTop
                     animated:NO];
deceze
  • 510,633
  • 85
  • 743
  • 889
John
  • 238
  • 2
  • 11