Search code examples
iosobjective-cuitableviewuiscrollviewuiscrollviewdelegate

Maintaining scroll position after reload in tableView


I've got a tableView. When user taps one of records, I check it's property and basing on this I'm filtering results and showing only similar results - so I need to refresh the tableView.

The problem is that user can scroll up/down the tableView. I need to scroll the tableView so the cell is exactly at the same UITableViewScrollPosition as before the refresh.

Obviously, I'm saving last tapped item

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    _lastSelectedItem = [self itemForIndexPath:indexPath];
} 

Then after reloading the tableView:

NSIndexPath *indexPath = [self indexPathForItem:_lastSelectedItem];
if (_tableView.numberOfSections > indexPath.section && [_tableView numberOfRowsInSection:indexPath.section] > indexPath.row) {
    [_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
    _lastSelectedItem = nil;
}

It would be good but... user could not finish at UITableViewScrollPositionMiddle. He could have finish scrolling at UITableViewScrollPositionTop or UITableViewScrollPositionBottom or even somewhere between.

-- edit --

Calculating via offset is also problematic, as the offset is the difference between start of view and top table scroll position.. while I'm not interested in this value :/.


Solution

  • Because my structure was quite complicated I finished to do it this way (eg. expandable sections etc). Please do mind, that I'm saving _lastSelectedItem, which is my object at datasource, as indexPath will change after refresh.

    - (void)refresh {
        [self saveLastScrollPosition];
        [self reloadData]; // inside we reload data + reload tableView
        [self scrollToLastScrollPosition];
    }
    
    - (NSInteger)heightToMinYOfCellAtIndexPath:(NSIndexPath *)indexPath {
        NSInteger sections = indexPath.section;
        NSInteger totalRows = 0;
        for (NSInteger section = 0; section < indexPath.section; section++) {
            totalRows += [self.tableView numberOfRowsInSection:section];
        }
        totalRows += indexPath.row;
    
        return ((sections + 1) * kHeaderHeight + totalRows * kRowHeight);
    }
    
    - (void)saveLastScrollPosition {
        if (_lastSelectedItem) { // make sure we have that item selected
            NSIndexPath *indexPath = [self itemForItem:_lastSelectedItem];
            NSInteger aboveItemHeight = [self heightToMinYOfCellAtIndexPath:indexPath];
            CGFloat contentOffsetY = self.tableView.contentOffset.y;
            _previousOffset = aboveItemHeight - contentOffsetY;
        }
    }
    
    - (void)scrollToLastScrollPostion {
        if (_lastSelectedItem) { // make sure we have that item selected
            NSIndexPath *indexPath = [self itemForItem:_lastSelectedItem];
            if (self.tableView.numberOfSections > indexPath.section && [self.tableView numberOfRowsInSection:indexPath.section] > indexPath.row) { // make sure the indexPath still exist after reload
                NSInteger aboveItemHeight = [self heightToMinYOfCellAtIndexPath:indexPath]; // height of items above selected index
                CGPoint offset = CGPointMake(0.f, aboveItemHeight - _previousOffset);
                // in case index should be higher: eg earlier item was at index (4,8) now is at (0,0)
                if (offset.y < 0) {
                    offset.y = 0;
                }
                [self.tableView setContentOffset:offset animated:NO]; // just jump, user shouldn't see this
                _previousOffset = 0; // forget the calculated values
                _lastSelectedItem = nil;
            }
        }
    }