views:

115

answers:

1

Hi all!

First of all, sorry for the too long question.

I know that there are few questions here that discuss similar issues but none of these talks about NSFetchedResultsController with delegate together with update in separate thread. And none of the solutions has helped me.
These are the existing questions:

Now about my problem:

  • I have a separate thread that updates the core data objects from the web (using a socket).
  • There are few view controllers that display the data from the same core data object (each tab contains a view controller that displays its filtered data).
  • Each view controller has its own instance of NSFetchedResultsController and the delegate is set to self.

Sometimes I receive was mutated while being enumerated exception on updating the data in the separate thread and sometimes it crashes the app.

I have done many code manipulations in order to try to fix it and seems that nothing helps.
I have tried not to use the managed object directly from table view datasource methods. Instead of that I have created an array which holds a list of dictionaries. i fill those dictionaries in the didChangeObject method from above. This way I don't touch the managed objects at all in the view controller.

And then I have understood that the problem is in NSFetchedResultsController that, probably, iterates the data all the time. And this is the object that conflicts with my data update in the separate thread.

The question is how can I update the core data objects in the separate thread once I have a NSFetchedResultsController with delegate (meaning that it "watches" the data and updates the delagate all the time).

NSFetchedResultsControllerDelegate implementation:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    if ( self.tabBarController.selectedIndex == 0 ) {
        UITableView *tableView = self.tableView;
        @try {
            switch(type) 
            {
                case NSFetchedResultsChangeInsert:
                    [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                case NSFetchedResultsChangeDelete:
                    [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                case NSFetchedResultsChangeUpdate:
                    [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
                    break;
                case NSFetchedResultsChangeMove:
                    [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }
        }
        @catch (NSException * e) {
            NSLog(@"Exception in didChangeObject: %@", e);
        }
    }
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    if ( self.tabBarController.selectedIndex == 0 ) {
        @try {
            switch(type) {
                case NSFetchedResultsChangeInsert:
                    [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                case NSFetchedResultsChangeDelete:
                    [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }           
        }
        @catch (NSException * e) {
            NSLog(@"Exception in didChangeSection: %@", e);
        }
    }
}

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

In the table view datasource methods I work directly with the managed object.

+1  A: 

Two separate questions here. First, if you are getting mutating errors that means you are mutating a set or array (or relationship) while iterating over that set/array/relationship. Find where you are doing that and stop doing it. That is the only solution.

As for your updates. Your background NSManagedObjectContext should be saving periodically. Your main thread should be listening for NSManagedObjectContextDidSaveNotification and when it receives one it calls the main NSManagedObjectContext on the main thread (as the notification will most likely come in on the background thread) via -mergeChangesFromContextDidSaveNotification: which takes the NSNotification as a parameter. This will cause all of your NSFetchedResultController instances to fire their delegate methods.

Simple as that.

Update

ank you for your reply. The exception is thrown on updating the NSManagedObjectContext in the background thread. I use the same NSManagedObjectContext in both threads. The app should be as close as possible to real time app - the updates constantly and the tables should be updated immediately. I don't save at all - I only update the NSManagedObjectContext. I have seen in one of the questions mentioned that someone used to separate instances of NSManagedObjectContext but he still receive the same exceptions once he merges the changes. So, you suggest using 2 separate NSManagedObjectContext's?

First, read up on multi-threading in Core Data from Apple's documentation (or my book :).

Second, yes you should have one context per thread, that is one of the golden rules of Core Data and multi-threading (the other is don't pass NSManagedObject instances across threads). That is probably the source of your crash and if it isn't it is going to be the source of a crash in the future.

Update

I have tons of data and I update only the modified/new/deleted items in the table. If I will start saving then will it harm the performance?

No, only the updates will be propagated across the threads. The entire data store will not be re-read so it will actually improve performance when you break up the saves into smaller chunks because you will be on the main thread, updating the UI, in smaller chunks so the UI will appear to perform better.

However, worrying about performance before the app is completed is a pre-optimization that should be avoided. Guessing at what is going to perform well and what won't is generally a bad idea.

Marcus S. Zarra
Thank you for your reply. The exception is thrown on updating the NSManagedObjectContext in the background thread. I use the same NSManagedObjectContext in both threads. The app should be as close as possible to real time app - the updates constantly and the tables should be updated immediately. I don't save at all - I only update the NSManagedObjectContext. I have seen in one of the questions mentioned that someone used to separate instances of NSManagedObjectContext but he still receive the same exceptions once he merges the changes. So, you suggest using 2 separate NSManagedObjectContext's?
Michael Kessler
I have tons of data and I update only the modified/new/deleted items in the table. If I will start saving then will it harm the performance?
Michael Kessler
Thank you Marcus. I will try your suggestions next week. BTW, when you edit your answer I don't see it in my profile - I usually also add a comment when I edit my answer...
Michael Kessler