Search code examples
iosobjective-cuitableviewnsfetchedresultscontroller

UITableViewCell header display drawing issue with NSFetchedResultsController delegate using NSManagedContextDidSaveNotification implementation


I have implemented an NSFetchedResultsController delegate with NSManagedContextDidSaveNotification to push managed objects changes from another NSManagedObjectContext connected to a common NSPersistentStoreCoordinator.

When the managed objects are first batch imported and after the NSFetchedResultsControllerDelegate methods are called, the result of the cell drawing contains a section that looks like this:

enter image description here

It is basically a display/drawing bug where a section header gets drawn into a cell. That header and cell are actual valid header and cell that appear further down.

It only happens the first time managed objects are created in batch. If I restart the app and the managed objects are already imported the controller displays everything fine so it's likely something to do with the import process, which is just the typical NSFetchedResultsControllerDelegate implementation (pasted below):

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [_tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:
     (id <NSFetchedResultsSectionInfo>)sectionInfo 
     atIndex:(NSUInteger)sectionIndex 
     forChangeType:(NSFetchedResultsChangeType)type {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [_tableView insertSections:
                [NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade]; break;
        case NSFetchedResultsChangeDelete:
            [_tableView deleteSections:
                [NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade]; break;
        case NSFetchedResultsChangeUpdate:
            NSLog(@"change section"); break;
        case NSFetchedResultsChangeMove:
            NSLog(@"move setion"); break;
    }
}
- (void)controller:(NSFetchedResultsController *)controller 
    didChangeObject:(id)anObject atIndexPath:
    (NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [_tableView insertRowsAtIndexPaths:
              [NSArray arrayWithObject:newIndexPath]
              withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [_tableView deleteRowsAtIndexPaths:
              [NSArray arrayWithObject:indexPath] 
              withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[_tableView cellForRowAtIndexPath:indexPath] 
            atIndexPath:indexPath];
            NSLog(@"change object"); break;
        case NSFetchedResultsChangeMove:
            [_tableView deleteRowsAtIndexPaths:
              [NSArray arrayWithObject:indexPath] 
              withRowAnimation:UITableViewRowAnimationFade];
            [_tableView insertRowsAtIndexPaths:
              [NSArray arrayWithObject:newIndexPath] 
              withRowAnimation:UITableViewRowAnimationFade];
            NSLog(@"move object"); break;
    }
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [_tableView endUpdates];
}

The thing is I tried just implementing a refresh with needsLayout and reloadData by detecting when it's a first batch import and calling them doesn't get rid of this display issue. HALP!

EDIT:

 - (void)setupFetchedResultsController {
    NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:@"Object"];
    NSSortDescriptor *sd1 = [NSSortDescriptor sortDescriptorWithKey:@"attribute" ascending:YES];
    NSSortDescriptor *sd2 = [NSSortDescriptor sortDescriptorWithKey:@"attribute2" ascending:YES];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.relationship.attribute3 == true"];
    [fr setPredicate:predicate];
    [fr setSortDescriptors:@[sd1, sd2]];
    _frc = [[NSFetchedResultsController alloc] initWithFetchRequest:fr managedObjectContext:_moc sectionNameKeyPath:@"attribute" cacheName:nil];
    [_frc setDelegate:self];
    NSError *error; if (![_frc performFetch:&error]) NSLog(@"%@", error.description);
}

EDIT: Here is the code that fixed the issue:

- (void)subscribeToNotifications {
    [[NSNotificationCenter defaultCenter] addObserver:self 
      selector:@selector(mergeManagedObject:) 
      name:NSManagedObjectContextDidSaveNotification
      object:nil];
}
- (void)mergeManagedObject:(NSNotification *)notification {
    dispatch_async(dispatch_get_main_queue(), ^{
        // This block needed to be sent on the main thread!
        [_managedObjectContext 
          mergeChangesFromContextDidSaveNotification:notification];
    });
}

Solution

  • Did you try implementing/adjusting your code as follows?

    • call - reloadSections:withRowAnimation: when NSFetchedResultsChangeUpdate on section changes,
    • - moveSection:toSection: when NSFetchedResultsChangeMove happens on section changes,
    • - moveRowAtIndexPath:toIndexPath: when NSFetchedResultsChangeMove is triggered on controller:didChangeObject:, and
    • - reloadRowsAtIndexPaths:withRowAnimation: when NSFetchedResultsChangeUpdate is triggered on controller:didChangeObject:.

    If that doesn't help, step into those methods and look on what thread they're called. Often, view bugs happen when view updates are called on threads other than the main thread.

    Last, did you try using childContexts?