How to keep UITableView contentoffset after calling -reloadData
Asked Answered
F

13

67
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?

Fanfani answered 27/12, 2011 at 1:31 Comment(1)
See here. This answer works better than all answers provided here. Even Matt Koala's answer my not work 100% of the time :)Hurds
C
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.

Canica answered 27/12, 2011 at 2:28 Comment(8)
yes, you're right! just another call scrollToRowAtIndexPath:atScrollPosition:animated: before i use reloadData.Fanfani
reloadData does not change contentOffset, but offset is changed relatively to visible cells.Chloe
this didn't work for me, I had to store the contentOffset and set it again right after reload.Twinberry
Why this is the accepted answer. I see no solution here.Pteridology
UITableViewAutomaticDimension may cause a problem with the offset. This answer also is not answer to question.Excretion
I can't agree with this answer. In my call trace I see that scrollViewDidScroll invocation was caused by tableView.reloadData(): and offset actually changed.Macdougall
OMG he is right! The scroll position is maintained on UICollectionView.Scout
it could shrink to maintain changes, but this redisplays only visible cells. so, if they dont changed, it shouldn't scrollCondole
F
123

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];
Falkirk answered 9/7, 2015 at 17:14 Comment(9)
I don't understand the accepted answer. I see reloadData definitely shifting my content offset and this helped it.Rabbinate
layoutIfNeeded() itself worked for me after reloadData() (Swift 3.0)Coercive
[tableView layoutIfNeeded]; solved my problem. Thank you!Ripple
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.Stephanotis
The tableView gets stuck lower on the screen if it is reloaded while scrolling using this solutionMelchizedek
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.Gono
@Gono is the user looking at the last row? If so then yes it does make sense to keep his original offset!Hurds
just make sure you don't call this inside an animation block. The result would be awkward :/Hurds
Did NOT work, It fixes the problem on iOS 12 but it doesn't on iOS13Surveying
P
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.

Pairoar answered 22/1, 2016 at 5:32 Comment(7)
But I have one improvement! Save the indexPath itself instead of just the row. Since I have multiple sections this was a problem...Cymbiform
@Cymbiform Yes. If you have multiple sections, your suggestion would be helpful. Thanks for thatPairoar
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.Alyse
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.Thicken
Thanks , this should be set to be the accept answer , set estimatedRowHeight to 0 solved my problem of contentOffset changing when tableView reloadDataArchdeacon
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.Mclin
This answer works for me, while the accepted answer doesn't.Thigmotaxis
D
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)
    }
}
Domenic answered 10/10, 2017 at 22:27 Comment(9)
Thanks, setting the estimated height worked for me. I had only implemented heightForRowAt indexpath.Purse
Thx a lot. This fix the issue for me. I am testing in iOS 11Mcgruder
First option is fix my issue. Thanks ( tested in iOS 11 & swift 4.1 )Greater
The problem with the first solution is that, if the new cell/item with different height is inserted at the top, the system will use the cached cell height. I haven't tried it, but I think this could be solved by invalidating cached cell height when the cell gets reused (look at prepareForReuse).Unwitting
You saved my day!Terrilynterrine
second solution is working specially when you are adding items to top of UICollectioinView, thanksMicroscopium
In the fist solution, you have to use IndexPath as dictionary key for tableview with more than one sectionHexastich
the second answer is the only thing that worked in my case (collection view), thank you very much bro! :)Painkiller
move of the masterBlowhard
B
29

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

fix reload problem

Bois answered 28/3, 2018 at 15:51 Comment(4)
Thank you so much ! I wasted too much time on this problem...Janayjanaya
Great Answer. But how?Wriest
Man, give me your address... I will ship you a bottle of whiskey!Alaster
You are the best!Seasonseasonable
K
28

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

Ko answered 21/8, 2015 at 8:41 Comment(4)
How to set estimatedRowHeight correctly if the height of cell is dynamic for each?Domenic
@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.Ko
@Ko 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?Ravens
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.Ko
L
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.
}
Lonee answered 27/7, 2018 at 3:13 Comment(1)
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.Rostov
R
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)
}
Rectangle answered 30/11, 2017 at 11:28 Comment(1)
You need to place tableView.layoutIfNeeded() after tableView.reloadData(). This will give you the updated currentContentHeight.Hornbeck
C
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.

Canica answered 27/12, 2011 at 2:28 Comment(8)
yes, you're right! just another call scrollToRowAtIndexPath:atScrollPosition:animated: before i use reloadData.Fanfani
reloadData does not change contentOffset, but offset is changed relatively to visible cells.Chloe
this didn't work for me, I had to store the contentOffset and set it again right after reload.Twinberry
Why this is the accepted answer. I see no solution here.Pteridology
UITableViewAutomaticDimension may cause a problem with the offset. This answer also is not answer to question.Excretion
I can't agree with this answer. In my call trace I see that scrollViewDidScroll invocation was caused by tableView.reloadData(): and offset actually changed.Macdougall
OMG he is right! The scroll position is maintained on UICollectionView.Scout
it could shrink to maintain changes, but this redisplays only visible cells. so, if they dont changed, it shouldn't scrollCondole
Q
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
}
Quickel answered 24/8, 2016 at 2:15 Comment(1)
However: this does not seem to work if you are animating between two layouts, unfortunately.Mazur
C
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

Crisper answered 9/12, 2016 at 4:12 Comment(0)
S
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.

Seychelles answered 29/10, 2019 at 16:1 Comment(0)
P
-3

This is working 100%

change the tableView.reloadData() 

into

tableView.reloadRows(at: tableView!.indexPathsForVisibleRows!, with: .none)
Parallax answered 22/9, 2017 at 2:49 Comment(0)
P
-4

For it works fine

[tView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
             atScrollPosition:UITableViewScrollPositionTop
                     animated:NO];
Planetoid answered 29/11, 2016 at 11:52 Comment(3)
Hi Deceze, can you give me why it is marked as down wardPlanetoid
May be because it is not solution for problem. Id is only solution to move a first cell to the top position.Crisper
Please read the question before answering. This is not a solution to the problem posed.Thicken

© 2022 - 2024 — McMap. All rights reserved.