0

I'm running into some trouble using background threads to update data in UIManagedDocument / Core Data. Specifically, I'm using a NSFetchResultsController to update map annotations based on geocoded data from a background thread (after merging back into my main MOC), but the map never updates because of the way UIManagedDocument commits data to its stores and/or to its multiple MOCs (parent and child). If I close the app and reopen, the annotations are populated, so a commit to the persistent store is occuring at some point, but it's unclear to me how to force such a commit which would thus update the NSFetchResultsController. Here's some code:

The background thread that updates the MOC:

- (void) populateGPSCoordsInClubsInContext: (NSManagedObjectContext *) mainCtx
{            
    dispatch_queue_t MapFetchQ = dispatch_queue_create("Google Map Data Fetcher", NULL);
    dispatch_async(MapFetchQ, ^{

        NSManagedObjectContext * ctxThread = [[NSManagedObjectContext alloc] init];
        [ctxThread setPersistentStoreCoordinator:mainCtx.persistentStoreCoordinator];


        NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Club"];
        request.predicate = [NSPredicate predicateWithFormat:@"inRegion.name=%@", self.name];
        NSError *error = nil;

        NSArray * clubs = [ctxThread executeFetchRequest:request error:&error];

        NSLog(@"[%@] Fetching map data. Club count is %d", self.name, [clubs count]);  

        int delayCounter = 0;

        for(Club * club in clubs)
        {
            if(![club.hasCoord boolValue] && club != nil)
            {
                delayCounter++; // to deal with google maps api's DoS protection            

                [club setLongitudeAndLattitudeFromGoogle];
                NSError * error;

                if(![ctx save:&error])
                 NSLog(@"[%@] Problem saving region to database.", self.name);

            }

            if(delayCounter == 8)
            {            
                [NSThread sleepForTimeInterval:(NSTimeInterval)2.0];
                delayCounter = 0;
            }
        }
    });
    dispatch_release(MapFetchQ);
}

When those saves are called, I grab the notification on the main thread (in my app delegate) like so:

- (void) contextDidSave: (NSNotification *) notification
{
    NSManagedObjectContext * ctx = [self.clubsDB managedObjectContext];
    [ctx mergeChangesFromContextDidSaveNotification:notification];

    NSArray * updates = [[notification.object updatedObjects] allObjects];

    for(Club * club in updates) // This never fires because updates never has objects
    {
        NSLog(@"*********** %@", club.name);
    }

    NSLog(@"[%@] %@", [self class], NSStringFromSelector(_cmd));

}

And I've set my fetched results controller like so (the predicate is correct, results are as expected with app reboot after data has been committed to the store):

-(void) setupFRC
{

    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Club"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];
    request.predicate = [NSPredicate predicateWithFormat:@"inRegion.name=%@ AND hasCoord=%@",[self.clubsDB regionTitleAsString], [NSNumber numberWithBool:YES]]; // Follow the relationshop and only display clubs from THIS region.

    //request.predicate = [NSPredicate predicateWithFormat:@"inRegion.name=%@",[self.clubsDB regionTitleAsString]];

    self.debug = YES;

    self.fetchedResultsController = 
    [[NSFetchedResultsController alloc] initWithFetchRequest:request 
                                        managedObjectContext:self.clubsDB.managedObjectContext
                                          sectionNameKeyPath:nil
                                                   cacheName:nil];
}

Any ideas as to how I can update the appropriate MOC to get the fetched results controller to behave as desired?

Andrew
  • 4,953
  • 15
  • 40
  • 58
Andrew
  • 1
  • 2

3 Answers3

0

OK, I'm going to edit your code to give you an idea of what it should look like. I assume you are passing the MOC from the UIManagedDocument into populateGPSCoordsInClubsContext. Note, there is very little difference in what you are already doing, but as we know, one line of code can make all the difference...

// NOTE: Make it clear you expect to work on a document...
- (void) populateGPSCoordsInClubsInContext: (UIManagedDocument *) document
{            
    dispatch_queue_t MapFetchQ = dispatch_queue_create("Google Map Data Fetcher", NULL);
    dispatch_async(MapFetchQ, ^{

        NSManagedObjectContext * ctxThread = [[NSManagedObjectContext alloc] init];
        // NOTE: Make changes up into the context of the document
        ctxThread.parentContext = document.managedObjectContext;    

        NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Club"];
        request.predicate = [NSPredicate predicateWithFormat:@"inRegion.name=%@", self.name];
        NSError *error = nil;

        NSArray * clubs = [ctxThread executeFetchRequest:request error:&error];

        NSLog(@"[%@] Fetching map data. Club count is %d", self.name, [clubs count]);  

        int delayCounter = 0;

        for(Club * club in clubs)
        {
            if(![club.hasCoord boolValue] && club != nil)
            {
                delayCounter++; // to deal with google maps api's DoS protection            

                [club setLongitudeAndLattitudeFromGoogle];
                NSError * error;

                // NOTE: This notifies the parent context of the changes.
                if(![ctx save:&error])
                 NSLog(@"[%@] Problem saving region to database.", self.name);
                // NOTE: However, since a UIManagedDocument is an "auto-save"
                // document, we need to tell it that is is dirty...
                [document updateChangeCount:UIDocumentChangeDone];
            }

            if(delayCounter == 8)
            {            
                [NSThread sleepForTimeInterval:(NSTimeInterval)2.0];
                delayCounter = 0;
            }
        }
    });
    dispatch_release(MapFetchQ);
}

One of the cool things about doing it this way, is that you don't have to even handle the notifications (at least not for consistency).

If you want to do it the other way, you can do this...

        ctxThread.parentContext = document.parentContext;    

You would not, then, call updateChangeCount on the document. These changes would go into the parent context, and into the file. However, doing this, you don't have to process the notifications either, because future fetches would automatically see them. Of course, if you want to refresh on changes, you could still process notifications, but that all. To see them on fetch, you don't have to do anything else.

Contrary to popular belief, UIManagedDocument is really quite effective and easy (once you know the rules).

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
0

I have a problem with the way UIManagedDocument sends its commits as well. The only solution I can think of is to stop using UIManagedDocument, and just use the context from the PersistentStore as provided in the default Master-Detail template.

edit: After further research, it seems like there isn't a way to for UIManagedDocument to commit changes, so it's probably better that you pass in a context created from the persistent store. It seems that it's no coincidence Apple hasn't provided any usable sample code for UIManagedDocument yet. I would stick with the default template.

Here's a link to someone facing a similar problem, and their "solution" - sometimes the best solution is to know there isn't one :P

Core Data managed object does not see related objects until restart Simulator

Community
  • 1
  • 1
Jack L.
  • 1
  • 2
0

I was actually able to solve the issue. The trick was to ensure that to context was pre-populated with the objects I was editing prior to setting the FRC. Frankly, it was extremely esoteric and the fact that UIManagedDocument doesn't work as expected (or even as the documentation explains it does) is disconcerting.

Andrew
  • 1
  • 2