9

Whenever I call

reloadRowsAtIndexPaths

My UITableView contentOffset is removed, is there a delegate method I can use to catch the table view updating and set the Offset again so that it remains in place and does not animate into view, or simply prevent it doing this?

I am setting the contentOffest in viewDidLoad:

self.tableView.contentOffset = CGPointMake(0, 43);

Here is an example usage:

[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[params valueForKey:@"index"], nil] withRowAnimation:UITableViewRowAnimationFade];

Which removes the contentOffset and animates it into view, which I do not want.

More specifically this appears to occur when the row being reloaded is at indexPath.section 0 and indexPath.row 0, i.e. the top row.


More Information

I am calling reloadRowsAtIndexPaths after an asynchronous request to fetch an image from a server. It basically works like so:

  • cellForRowAtIndexPath is called which checks for the presence of the thumb file on the disk, if the file is not present a placeholder is loaded in it's place and an asynchronous request is started in a background thread to fetch the image.
  • When the image download has completed I call reloadRowsAtIndexPaths for the correct cell so that the correct image fades in in place of the placeholder image.
  • The amount of cells may be different as the request is called inside cellForRowAtIndexPath so that the images load in as the cells load

cellForRowAtIndexPath file check

paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_small.gif",[[listItems objectAtIndex:indexPath.row] valueForKey:@"slug"]]];

    if([[NSFileManager defaultManager] fileExistsAtPath:path]){

        listCell.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfFile:path]];

    } else {

        listCell.imageView.image = [UIImage imageNamed:@"small_placeholder.gif"];

        NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[[[listItems objectAtIndex:indexPath.row] valueForKey:@"image"] valueForKey:@"small"],@"image",[NSString stringWithFormat:@"%@_small.gif",[[listItems objectAtIndex:indexPath.row] valueForKey:@"slug"]], @"name",indexPath,@"index",@"listFileComplete",@"notification",nil];

        [NSThread detachNewThreadSelector:@selector(loadImages:) toTarget:self withObject:params];

        [params release];

    }

File donwloaded notification:

-(void)fileComplete:(NSNotification *)notification {

    [self performSelectorOnMainThread:@selector(reloadMainThread:) withObject:[notification userInfo] waitUntilDone:NO];
}

Cell reload ( I have hardcoded sections due to a bug with strange section numbers being passed very rarely causing a crash:

-(void)reloadMainThread:(NSDictionary *)params {

    NSIndexPath *index;

    switch ([[params valueForKey:@"index"] section]) {
        case 0:
            index = [NSIndexPath indexPathForRow:[[params valueForKey:@"index"] row] inSection:0];
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[params valueForKey:@"index"], nil] withRowAnimation:UITableViewRowAnimationNone];
            break;
        default:
            index = [NSIndexPath indexPathForRow:[[params valueForKey:@"index"] row] inSection:2];
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:index,nil] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }

}
mkj
  • 2,761
  • 5
  • 24
  • 28
Alex
  • 2,513
  • 5
  • 27
  • 42

5 Answers5

3

Sounds like you are running into this problem due to incorrectly estimated row heights. Because (for some mysterious reason) the table view determines the new offset after reloading some cells using the estimated row height you want to make sure the tableView:estimatedHeightForRowAtIndexPath returns correct data for cells that have already been rendered. To accomplish this you could cache the seen row heights in a dictionary:

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    heightForIndexPath[indexPath] = cell.frame.height
}

then use this correct data or your estimate for not already loaded cells:

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return heightForIndexPath[indexPath] ?? averageRowHeight
}

(HUGE thanks to eyuelt for the insight that estimated row height is used to determine the new offset.)

Daniel Schlaug
  • 1,504
  • 1
  • 13
  • 17
  • Wowowowow! Best answer, I wish I could upvote it 10x!!! Thanks a lot, Daniel, I hope in the future you will find one of my answer as useful as I found yours now – Balazs Nemeth Aug 10 '16 at 15:37
  • Wowwwwwwwwwwwwwww....I search high and low for a solution to my problem of overlapping cells and this answer APPEARS to have solved it. Much love, Daniel! – Nick Coelius Feb 13 '18 at 18:08
3

EDIT: My original answer may not have focused on the core problem

Are you changing your number of rows before the call to reloadRows...? reloadRows... is specifically to animate a value change, so your code should look something like this:

UICell *cell = [self cellForRowAtIndexPath:indexPath];
cell.label.text = @"Something new";
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]];

Is that more or less what you look like, but the tableview is forgetting where it is?


Previous discussion

You don't call -beginUpdates and -endUpdates around a reload. You call -beginUpdates and -endUpdates around your related modifications of the backing data, during which you should be calling -insertRowsAtIndexPaths:withRowAnimation: and its relatives. If you call the updating routines, then you don't need to call the reload routines. Reloading in the middle of an update is undefined behavior.

See Batch Insertion, Deletion, and Reloading of Rows and Sections for details on when you use -beginUpdates.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks for the tip, i guess my problem is with reloadRowsAtIndexPaths and not beginUpdates as the problem still exists with begin/end removed – Alex May 24 '11 at 14:50
  • Edited; I may have misunderstood what the rest of the code looks like. – Rob Napier May 24 '11 at 14:57
  • i have updated my question to give some more insight into my problem – Alex May 24 '11 at 15:11
  • a slight varient on your solution did the trick, instead of calling reloadRowsAtIndexPaths though i simply called setNeedsDisplay – Alex May 24 '11 at 17:00
0

Replace

[_tableView reloadData];

with

[_tableView  reloadRowsAtIndexPaths:...withRowAnimation:...];

and it will be good.

Pang
  • 9,564
  • 146
  • 81
  • 122
0

I use this:

-(void)keepTableviewContentOffSetCell:(cellClass *)cell{
    CGRect rect = cell.frame;
    self.tableView.estimatedRowHeight = rect.size.height ;
}

You can use it before you reload the cell. The table view won't scroll when reload cell.

Pang
  • 9,564
  • 146
  • 81
  • 122
0

Have you tried the following:

CGPoint offset = [self.tableView contentOffset];
CGSize size = [self.tableView contentSize];

CGFloat percentScrolled = offset.y / size.height;

// call reloadRowsAtIndexPaths, inserts, deletes etc.
// ...

CGSize newSize = [self.tableView contentSize];
CGSize newOffset = CGPointMake(0, newSize.height * percentScrolled);

[self.tableView setContentOffset:newOffset animated:NO];

This seems a little too trivial, not sure it would work, but worth a shot.

octy
  • 6,525
  • 1
  • 28
  • 38