Search code examples
ioscore-datansmanagedobjectgrand-central-dispatchnsmanagedobjectcontext

Fetching Core Data objects in the background: objects not faulted


I need some help in using objects from Core Data with GCD; I seem to get NSManagedObjects that are aren't faulted into the main thread, even when I access their properties. Would appreciate some help.

This is what I'm doing: on launch, I need to load a list of Persons from the Core Data DB, do some custom processing in the background, then reload the table to show the names. I am following the guidelines for Core Data multi-threading by only passing in the objectIDs into the GCD queues. But when I reload the tableview on the main thread, I never see the name (or other properties) displayed for the contacts, and on closer inspection, the NSManagedObjects turn out to be faults on the main thread, even though I access various properties in cellForRowAtIndexPath. The name property is visible in the background thread when I NSLog it; and it's also showing correctly on the main thread in NSLogs in cellForRowAtIndexPath. But they don't show in the tableView no matter what I do. I tried accessing the name property using the dot notation, as well as valueForKey, but neither worked.

Here's my code …. it's called from the FRC initializer:

- (NSFetchedResultsController *)fetchedResultsController 
{
    if (__fetchedResultsController != nil) 
    {
        return __fetchedResultsController;
    }

    __fetchedResultsController = [self newFetchedResultsControllerWithSearch:nil]; // creates a new FRC

    [self filterAllContactsIntoDictionary: __fetchedResultsController];
    return [[__fetchedResultsController retain] autorelease];
}  


- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{

    NSArray *fetchedIDs = [[frc fetchedObjects] valueForKey:@"objectID"];
    NSArray *fetched = [frc fetchedObjects];

    if (filterMainQueue == nil) {
        filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
    }
    dispatch_async(self.filterMainQueue, ^{

        NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
        [backgroundContext setPersistentStoreCoordinator:[[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
        NSMutableArray *backgroundObjects = [[NSMutableArray alloc] initWithCapacity: fetchedIDs.count];

        // load the NSManagedObjects in this background context
        for (NSManagedObjectID *personID in fetchedIDs)
        {
            Person *personInContext = (Person *) [backgroundContext objectWithID: personID];
            [backgroundObjects addObject:personInContext];
        }
        [self internal_filterFetchedContacts: backgroundObjects]; // loads contacts into custom buckets

        // done loading contacts into character buckets ... reload tableview on main thread before moving on
        dispatch_async(dispatch_get_main_queue(), ^{

            CGPoint savedOffset = [self.tableView contentOffset];
            [self.tableView reloadData];
            [self.tableView setContentOffset:savedOffset];

        });
    });
}

What am I doing wrong here? Is there any other way to explicitly make the Person objects fire their faults on the main thread? Or am I doing something wrong with GCD queues and Core Data that I'm not aware of? Thanks.


Solution

  • Why not take the easy route, since you are not saving anything new ? Instead of creating an extra context for the background thread and working with IDs, use the main managedObjectContext in the background thread after locking it.

    for example:

    - (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
    {
    
        if (filterMainQueue == nil) {
            filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
        }
        dispatch_async(self.filterMainQueue, ^{
    
            NSManagedObjectContext *context = ... // get the main context.
            [context lock];     // lock the context.
    
            // do something with the context as if it were on the main thread.
    
            [context unlock];   // unlock the context.
    
            dispatch_async(dispatch_get_main_queue(), ^{
               CGPoint savedOffset = [self.tableView contentOffset];
               [self.tableView reloadData];
               [self.tableView setContentOffset:savedOffset];
           });
       });
    }
    

    This works for me when I call a method with performSelectorInBackground, so I guess it should work for GCD dispatch too.