Search code examples
ioscore-datansfetchedresultscontroller

NSFetchedResultController refresh table view too slow


I am writing an IM app by storing the conversation list in the Core Data, and display the data source via NSFetchedResultsController. The Core Data stack is copied from Apple's example code.

The NSFetchedResultsController is initialized as follows:

- (NSFetchedResultsController *)fetchedResultsController {
    if (!_fetchedResultsController) {
        NSManagedObjectContext *context = [[IMWrapper sharedInstance].chatManager managedObjectContext];
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        // Edit the entity name as appropriate.
        NSEntityDescription *entity = [Conversation entityInManagedObjectContext:context];
        [fetchRequest setEntity:entity];

        NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:ConversationAttributes.lastTime ascending:NO];
        NSArray *sortDescriptors = @[sortDescriptor];
        [fetchRequest setSortDescriptors:sortDescriptors];

        // Edit the section name key path and cache name if appropriate.
        // nil for section name key path means "no sections".
        NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
        aFetchedResultsController.delegate = self;
        _fetchedResultsController = aFetchedResultsController;

        NSError *error = nil;
        if (![_fetchedResultsController performFetch:&error]) {
            // Replace this implementation with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }

    return _fetchedResultsController;
}

When a message is received, I will update the unread count of the corresponding conversation which is stored in Core Data, and refresh the badge number on both the conversation and the corresponding tab bar item. However, the tab bar item's badge number is updated immediately, while the conversation's badge number is updated several seconds later. I am afraid that the NSFetchedResultsController does not fetch the new unread count in time. Following code shows my methods of NSFetchedResultsControllerDelegate.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        default:
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {
    UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

I hope anyone could help me. Thank you very much!

P.S. I searched in stack overflow, someone says it might be the thread problem. I print the thread information in didChangeObject method, and find that it is not in the main thread. Some answer tells to merge the update in moc to main thread. But I don't know how to.

The following code shows the update for the unread message count. The problem might lie there.

dispatch_async(self.messageQueue, ^{
    // Update unreadMessageCount here
    if (aMessage.isRead == NO) {
        // mogenerator provides this convenient method.
        self.unreadMessageCountValue += 1;
    }

    NSError *updateError = nil;
    if (![self.managedObjectContext save:&updateError]) {
        DLog(@"Unable to update managed object context.");
        DLog(@"%@, %@", updateError, updateError.localizedDescription);
    }

    // Add aMessage to message array....

    // Update tab tar badges via chat manager....
});

Solution

  • You're missing the line of code telling the table view to refresh a row when its corresponding managed object has changed. To the following method:

    - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath 
    

    Add the missing line, as follows:

           case NSFetchedResultsChangeUpdate:
                [self.savedLocsTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;
    

    That may be all that's needed. So, when the Conversation's unreadCount changes and is saved to Core Data, this method is called with NSFetchedResultsChangeUpdate and the corresponding table row will now be reloaded.


    UPDATE

    After seeing your code to update the message count, I think you do have a threading problem.

    If you did, indeed, copy the Core Data stack from Apple's example, you only have 1 moc and it is on the main thread. So, all you need to do is return to the main thread to modify any managed objects or do any work on the moc. (Note that neither moc's nor managed objects are threadsafe. If you create them on the main thread, you can only modify them on the main thread.)

    Also, I noticed that you are updating the tab bar in the background thread. You may also want to move this to the main thread. (AFAIK, most interactions with UIKit must be done on the main thread, with the exception of UIImage, UIColor, UIFont and Core Graphics, which are all threadsafe. However, it's possible that in newer versions of iOS modifying a tab bar is threadsafe, and your program seems to be working well in that area. So, proceed with caution.)

    Here is the code to return to the main thread:

    dispatch_async(self.messageQueue, ^{
        if (aMessage.isRead == NO) {
    
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update unread message count here
                // mogenerator provides this convenient method.
                self.unreadMessageCountValue += 1;
    
                NSError *updateError = nil;
                if (![self.managedObjectContext save:&updateError]) {
                    DLog(@"Unable to update managed object context.");
                    DLog(@"%@, %@", updateError, updateError.localizedDescription);
                }
            });
    
            // Add aMessage to message array....
    
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update tab bar badges (unless chat manager stuff must be done in messageQueue and cannot be separated from tab bar stuff)
            });
    
        }
    });
    

    So, both changes are needed: (1) handle NSFetchedResultsChangeUpdate as shown above and (2) make any changes to a NSManagedObject or to the moc ONLY on the main thread.