Sunday, December 5, 2010

iOS : Replace a SQLite database at runtime using Core Data

I have been developing an application that required the SQLite database file to be replaced at runtime. This event was triggered when a new database file was downloaded from a remote server. I considered forcing the user to close the application, however this would be a poor user experience and just poor form.

One major issue was releasing the existing database correctly and then re-connecting.  The last hurdle was to clear any existing references to any managed objects currently being viewed or cached.

After searching the net and some experimenting I finally have something that is working.

- (BOOL) updateDB {
 
 if (managedObjectContext != nil) {
  [managedObjectContext lock];
  [managedObjectContext reset];
 }
 
 if (persistentStore != nil) {
  NSError *error;
  if (![persistentStoreCoordinator removePersistentStore:persistentStore error:&error]) {
      NSLog(@"Unable to remove persistent store error %@, %@", error, [error userInfo]);
      return FALSE;     
  }
 }
 if (managedObjectContext != nil) { 
     [managedObjectContext unlock];
 }
 if (persistentStore != nil) {
     [persistentStoreCoordinator release], persistentStoreCoordinator = nil;
 }
 if (managedObjectContext != nil) {
     [managedObjectContext release], managedObjectContext = nil;
 }
 if (managedObjectModel != nil) {
     [managedObjectModel release], managedObjectModel = nil;
 }
 
 [self replaceDB];
 
 [self managedObjectContext];
 
 [self clearDBReferences];
 
 return TRUE;
}

The managedObjectContext is initialised lazily, therefore is could be nil.

- (NSManagedObjectContext *) managedObjectContext {
    
    if (managedObjectContext != nil) {
        return managedObjectContext;
    }
  
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator: coordinator];
    }
    return managedObjectContext;
}

The order of the messages are important to ensure the resources are released in the reverse order they were initialised.

The [self replaceDB] simple copies the new SQLite DB into place.

The [self managedObjectContext] simple forces the Lazily initialisation.

The last message races though all controllers requesting them to clear any references to managed objects.

I simple created a ReleaseResource protocol that has a clear method. If a controller implements this protocol then it can simple release any managed objects.

For example, I have a controller that subclasses UITableViewController.

-(void)clear {
    self.fetchedResultsController = nil;
    [self.tableView reloadData];
}

This code will obviously change depending on your application layout. This method races through the controllers looking for classes that implement the ReleaseResource protocol.

- (void) clearDBReferences {
    for (UIViewController *controller in tabBarController.viewControllers) {
  // for iPad
  if ([controller isKindOfClass:[UISplitViewController class]]) {
   NSLog(@"Clearing iPAD DB References");
   for (UINavigationController *navController in ((UISplitViewController *) controller).viewControllers) {
    [navController popToRootViewControllerAnimated:FALSE];
    for (UINavigationController *rootController in ((UINavigationController *) navController).viewControllers) {
     if ([rootController conformsToProtocol:@protocol(ReleaseResources)]) {
      [rootController performSelector:@selector(clear)];
     }
    }
   }
  }  
  // for iPhone
  if ([controller isKindOfClass:[UINavigationController class]]) {
   UINavigationController *navController = ((UINavigationController *) controller);
   NSLog(@"Clearing iPhone DB References");   
   [navController popToRootViewControllerAnimated:FALSE];
   for (UINavigationController *rootController in ((UINavigationController *) navController).viewControllers) {
    if ([rootController conformsToProtocol:@protocol(ReleaseResources)]) {
     [rootController performSelector:@selector(clear)];
    }
   }
  }
 }
}

6 comments:

  1. Thanks, Tate. This is exactly what I was looking for!

    ReplyDelete
  2. This is really great! NIce work dude :)

    ReplyDelete
  3. Hi Tate, thanks for the article!
    I am basically doing the same in my app for some time now.
    However, with increased code complexity I am running into issues: Sometimes I am missing some NSManagedObject instances in my cleaning process.

    This makes me think that the whole pattern (hunting down NSManagedObjects all across your running app) might be not quite right.

    What we are basically trying to do is propagating changes in our model down to the views – doing that kind of job is actually what Core Data is for, right?

    In my perfect world, I would change the PersistentStore which would then post some notification causing the ManagedObjectContext to reconnect managed objects to the new store, deleting those who are not there anymore, updating others and creating new ones.
    Then FetchedResultsControllers would be updated and could in turn propagate their changes to the view controllers via their delegates.

    All these notification patterns are there – they just don't seem to work like that, maybe they are not made for that case.
    I am not experienced enough with CoreData but I have the feeling we are missing something here.

    What do you think about this?
    Daniel

    ReplyDelete
  4. You are on the right track. I would create a protocol with two events. One to release and another to reload. Have your views/objects implement this protocol with require code to perform these operations. When these views are loaded have them register these events with NSNotificationCentre. Before you replace the database send a release notification and after send a reload.

    ReplyDelete