Background Loading of Data

Often applications have the need to import or export data and ideally these tasks should not block the User Interface while they are being performed.

On most modern platforms like iOS. Mac OSX, Windows and UNIX the user interfaces runs on a foreground thread and one should avoid blocking this thread with long jobs because it prevents the UI from responding to user interaction.

Usually one would make use of a background thread to process these long jobs and this becomes especially important on mobile platforms which have limited processing resources.

Core Data has a number of special requirements for multi-threaded processing (running concurrent jobs), these include:

  1. The applications main thread is used for managing the User Interface, which includes loading tableViews with data, scrolling these views and so on.  When populating the UI with Core Data objects it is necessary that the managedObjectContext you are using to access objects from the main thread is created on the main thread
  2. Core Data managedObjectContext’s are NOT thread safe so you CANNOT access objects from this context on other threads
  3. However you can create multiple managedObjectContexts on different threads and associated them with the same persistentStoreCoordinator.

So in order to have a background job to load or delete data you must:

  1. Create a managedObjectContext on the background thread
  2. Register for NSManagedObjectContextDidSaveNotifications so that you get notified on the main thread when a background thread saves.
  3. When you receive the NSManagedObjectContextDidSaveNotification perform a mergeChangesFromContextDidSaveNotification on the main managedObjectContext to merge the data changes in the main context
  4. Be careful when deleting objects because attempts to access retained objects that have been deleted on other threads will result in a ‘Fault could not be fulfilled’ exception.

Here is an outline of the code.

Creating the Thread

 
- (void)loadDataInBackground {

   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {

      [self loadData];

   });

}

- (void)loadData {

   FLOG(@"loadData called");

   _loadJobCount++;

   [self postJobStartedNotification];

   [self showBackgroundTaskActive];

   NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];

   // Register for saves in order to merge any data from background threads

   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(storesDidSave:) name: NSManagedObjectContextDidSaveNotification object:bgContext];

   bgContext.persistentStoreCoordinator = [self persistentStoreCoordinator];

   FLOG(@" starting load...");

   for (int i = 1; i<=5; i++) {

      // Add a few companies

      [self insertNewCompany:bgContext count:5];

      [bgContext processPendingChanges];

      // Save the context.

      NSError *error = nil;

      if (![bgContext save:&error]) {

         FLOG(@"  Unresolved error %@, %@", error, [error userInfo]);

      }

   }

   // Deregister for saves in order to merge any data from background threads

   [[NSNotificationCenter defaultCenter] removeObserver:self name: NSManagedObjectContextDidSaveNotification object:bgContext];

   FLOG(@" loading ended...");

   [self showBackgroundTaskInactive];

   _loadJobCount--;

   [self postJobDoneNotification];
}

Handing the Save Notification

// NB - this may be called from a background thread so make sure we run on the main thread !!

// This is called when transaction logs are loaded by Core Data or when we save from a background or main thread

- (void)storesDidSave:(NSNotification*)notification {

   // Ignore any notifications from the main thread because we only need to merge data
   // loaded from other threads.
   if ([NSThread isMainThread]) {
      FLOG(@" main thread saved context");
      return;
   }

   [[NSOperationQueue mainQueue] addOperationWithBlock:^ {

      FLOG(@"storesDidSave ");

      // Set this so that after the timer goes off we perform a save
      // - without this the deletes don't appear to trigger the fetchedResultsController delegate methods !

      _import_or_save = YES;
      [self createTimer];

      if (self.managedObjectContext) {
         [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
      }
   }];
}

- (void)createTimer {
   FLOG(@" called");

   if (self.iCloudUpdateTimer == nil) {
      self.iCloudUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(notifyOfCoreDataUpdates)
userInfo:nil
repeats:NO];

      // If we are using iCloud then show the network activity indicator while we process the imports

      if ([self isCloudEnabled])
        [self showBackgroundTaskActive];

   } else {

      [self.iCloudUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
   }
}

- (void)notifyOfCoreDataUpdates {
   FLOG(@"notifyOfCoreDataUpdates called");

   if (_storesUpdatingAlert)
      [_storesUpdatingAlert dismissWithClickedButtonIndex:0 animated:YES];
   [self.iCloudUpdateTimer invalidate];
   self.iCloudUpdateTimer = nil;

   // Do a save if we have received imports or Saves from other threads
   if (_import_or_save) {
      FLOG(@" import_or_save = YES");
      NSError *error;

      FLOG(@" saving moc");

      if (![self.managedObjectContext save:&error]) {
         FLOG(@" error saving context, %@, %@", error, error.userInfo      );
      }
      _import_or_save = NO;
   }
   [self postUIUpdateNotification];
   [self showBackgroundTaskInactive];
}

Leave a comment