Reputation: 536
I am trying to use Core Data in an app I'm working on; in the app delegate this code kicks off the import of data from a downloaded JSON file.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//setup app window, etc.
BECDRefreshOperation *loadData = [[BECDRefreshOperation alloc] initWithStoreCoordinator:self.persistentStoreCoordinator];
[queue addOperation:loadData]; //queue is an NSOperationQueue
return YES;
}
BECDRefreshOperation is a subclass of NSOperation that runs the import. It creates its own managed object context in order to separate the main one from the background process.
- (id) initWithStoreCoordinator:(NSPersistentStoreCoordinator *)storeCoordinator{
if (![super init]) return nil;
[self setPersistentStoreCoord:storeCoordinator];
return self;
}
- (void) main{
NSNumberFormatter *f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterDecimalStyle];
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:self.persistentStoreCoord];
//import the data from JSON
}
The actual import works and updates the data store; however, the table view controller which uses an NSFetchedResultsController does not update. The table view controller is the NSFetchedResultsControllerDelegate and contains all of the delegate methods.
On the second run of the app, the table view controller displays correctly because the data was previously loaded into the store; any updates made in the import do not refresh, however.
I have read the Core Data Concurrency guidelines from Apple multiple times as well as searched many times on Google and SO for the answer. I believe it lies in using mergeChangesFromContextDidSaveNotification, but I have tried to do this in many different places in both the app delegate and the table view controller by registering for the save notification and calling a method to merge changes, and none of what I have tried works. Cocoa is my GF's implementation is one of the ways I have tried to adapt in order to do this.
The table view controller is passed the app delegate's managedObjectContext when it is created.
I have run this without multithreading and the code to import into the data store and display in the table view controller works, but of course it freezes the UI while importing the data.
It's pretty obvious I'm doing something wrong here; any help will be greatly appreciated.
Update I added some NSLog statements and break points to see if the two managedObjectContexts were indeed pointing to the same memory address and it seems that they are, while the background MOC is at a different address. The notification code seems like it should work and update the main MOC, but thus far it is not.
2012-06-25 21:48:02.669 BE_CoreData[18113:13403] beerListViewController.managedObjectContext = <NSManagedObjectContext: 0x94233d0>
2012-06-25 21:48:02.671 BE_CoreData[18113:13403] appDelegate.managedObjectContext = <NSManagedObjectContext: 0x94233d0>
2012-06-25 21:48:02.722 BE_CoreData[18113:15003] backgroundMOC = <NSManagedObjectContext: 0x7b301b0>
Update 2 After additional troubleshooting it appears the NSFetchedController delegate methods are not firing. Here's the code for the NSFetchedResultsController and its delegate.
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:@"Beer" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:@"beertitle" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"beertitle"
cacheName:@"BeerTable"];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )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;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
Also here is the code for changeCell which is called when a cell needs updating:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Beer *beer = [_fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = beer.beertitle;
if (beer.beerthumb != nil){
[cell.imageView setImageWithURL:[NSURL URLWithString:beer.beerthumb]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
}
else {
[cell.imageView setImageWithURL:[NSURL URLWithString:@"http://beersandears.net/images/missing.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
}
}
Finally the fetchBeers method is called by viewDidLoad to actually perform the fetch.
- (void)fetchBeers{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
exit(-1); // Fail
}
}
Update 3
Testing to ensure the fetch occurs first. It does, but not by much (this was run on a 4S).
2012-06-28 20:47:37.214 BE_CoreData[3559:907] Fetch called
2012-06-28 20:47:37.281 BE_CoreData[3559:1103] Import started
2012-06-28 20:47:37.285 BE_CoreData[3559:1103] backgroundMOC = <NSManagedObjectContext: 0x1f03f050>
2012-06-28 20:47:39.124 BE_CoreData[3559:1103] call contextDidSave
2012-06-28 20:47:40.926 BE_CoreData[3559:1103] call contextDidSave
2012-06-28 20:47:42.071 BE_CoreData[3559:1103] call contextDidSave
2012-06-28 20:47:45.551 BE_CoreData[3559:1103] call contextDidSave
2012-06-28 20:47:45.554 BE_CoreData[3559:1103] Finished refresh operation
Instead of starting with a blank SQLlite store, I seeded a SQLlite store and ran through the same process. The seed loads correctly when it starts up, but the changes since the seed do not appear immediately in the table view. If you scroll to the spot where a row should be added before it's loaded (and it's not there), even after the import finishes it does not appear. However, scroll away and come back and the added row appears. It seems that when the database is empty it has nothing to scroll to and therefore doesn't add anything. With the seed it eventually adds them in but not in the way I've seen the core data stores work with an animated insert.
Upvotes: 0
Views: 1119
Reputation: 2779
As long as your context on the main thread is the same for app delegate and the view controller, it is just a design decision where you perform the merge.
The merge itself is pretty straightforward.
Here is the sample code how you perform the merge:
// Whatever method you registered as an observer to NSManagedObjectContextDidSave
- (void)contextDidSave:(NSNotification *)notification
{
[self.managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}
And note, you actually have to save on the background thread for the notification to fire.
Upvotes: 1