Note that there are some explanatory texts on larger screens.

plurals
  1. POCore Data Multiple Threads - View Controller Not Updating
    primarykey
    data
    text
    <p>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.</p> <pre><code>- (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; } </code></pre> <p>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.</p> <pre><code>- (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 } </code></pre> <p>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. </p> <p>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.</p> <p>I have read the <a href="http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/coredata/Articles/cdConcurrency.html#//apple_ref/doc/uid/TP40003385-SW1" rel="nofollow">Core Data Concurrency</a> 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. <a href="http://www.cimgf.com/2011/08/22/importing-and-displaying-large-data-sets-in-core-data/" rel="nofollow">Cocoa is my GF's implementation</a> is one of the ways I have tried to adapt in order to do this.</p> <p>The table view controller is passed the app delegate's managedObjectContext when it is created.</p> <p>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.</p> <p>It's pretty obvious I'm doing something wrong here; any help will be greatly appreciated.</p> <p><strong>Update</strong> 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.</p> <pre><code>2012-06-25 21:48:02.669 BE_CoreData[18113:13403] beerListViewController.managedObjectContext = &lt;NSManagedObjectContext: 0x94233d0&gt; 2012-06-25 21:48:02.671 BE_CoreData[18113:13403] appDelegate.managedObjectContext = &lt;NSManagedObjectContext: 0x94233d0&gt; 2012-06-25 21:48:02.722 BE_CoreData[18113:15003] backgroundMOC = &lt;NSManagedObjectContext: 0x7b301b0&gt; </code></pre> <p><strong>Update 2</strong> After additional troubleshooting it appears the NSFetchedController delegate methods are not firing. Here's the code for the NSFetchedResultsController and its delegate.</p> <pre><code>- (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]; } </code></pre> <p>Also here is the code for changeCell which is called when a cell needs updating:</p> <pre><code>- (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"]]; } } </code></pre> <p>Finally the fetchBeers method is called by viewDidLoad to actually perform the fetch.</p> <pre><code>- (void)fetchBeers{ NSError *error; if (![[self fetchedResultsController] performFetch:&amp;error]) { // Update to handle the error appropriately. NSLog(@"Unresolved error %@, %@", error, [error userInfo]); exit(-1); // Fail } } </code></pre> <p><strong>Update 3</strong></p> <p>Testing to ensure the fetch occurs first. It does, but not by much (this was run on a 4S).</p> <pre><code>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 = &lt;NSManagedObjectContext: 0x1f03f050&gt; 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 </code></pre> <p>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.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload