Search code examples
iosobjective-cuitableviewcore-datansfetchedresultscontroller

Make UITableView keep scroll position when adding cells at top


Ok so what I need is to prevent tableview scrolling when new elements added above current visible row. I'm using NSFetchedResultsController with a lot of objects (like 10 000) and reloadData works pretty smooth when I'm not touching contentOffset.

However, when I'm trying to manipulate contentOffset to keep scroll position when new entries are inserted it starts to freeze UI for like 300 ms which is bad for me.

Can anyone suggest any better (faster) solution of how to keep tableview at same cell when new items added at the top?

And this is the code that I'm using currently to track insertions and shift contentOffset. It doesn't support animations and different cell size.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [_insertions addObject:@(newIndexPath.row)];
            break;
    }
}


- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    _insertions = @[].mutableCopy;
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {        
    NSMutableArray *copy = _insertions.mutableCopy;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        NSSortDescriptor *lowestToHighest = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES];
        [copy sortUsingDescriptors:[NSArray arrayWithObject:lowestToHighest]];

        dispatch_sync(dispatch_get_main_queue(), ^{

            for (NSNumber *i in copy) {
                [self p_delayedAddRowAtIndex:[i integerValue]];
            }
            [self.tableView reloadData];
        });
    });
}

- (CGFloat)p_firstRowHeight {
    return [self tableView:[self tableView] heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0
                                                                                       inSection:0]];
}

-(void)p_delayedAddRowAtIndex:(NSInteger)row {

    NSIndexPath *firstVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] firstObject];

    if (firstVisibleIndexPath.row >= row) {

        CGPoint offset = [[self tableView] contentOffset];
        offset.y += [self firstRowHeight];

        if ([self.tableView visibleCells].count > self.tableView.frame.size.height / [self firstRowHeight]) {
            [[self tableView] setContentOffset:offset];
        }
    }
}

Solution

  • Ok, after some responses which pointed me in right direction I came up with this solution. It works smooth though does some ugly coordinates math and doesn't support different cell sizes. Hope it will be helpful for someone.

    -(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
        NSIndexPath *firstVisible = [[self.tableView indexPathsForVisibleRows] firstObject];
        self.topVisibleMessage = [self.fetchedResultsController objectAtIndexPath:firstVisible];
    
        NSIndexPath *topIndexPath = [self.fetchedResultsController indexPathForObject:self.topVisibleMessage];
    
        self.cellOffset =  self.tableView.contentOffset.y - topIndexPath.row * [self firstRowHeight];
    }
    
    
    -(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
        [self.tableView reloadData];
    
        if (self.topVisibleMessage) {
            NSIndexPath *topIndexPath = [self.fetchedResultsController indexPathForObject:self.topVisibleMessage];
            CGPoint point = {0, topIndexPath.row * [self firstRowHeight] + self.cellOffset};
    
            [self.tableView setContentOffset:point];
        }
    }