Progress with adding iCloud to Chef’s Book.

There are some interesting challenges with incorporating iCloud support, especially into an existing application. One, which I’ve been working through, is that you can no longer copy in a pre-existing store for the purposes of loading data. Such a move appears to work but does not upload anything to the cloud, the cloud store will only see future changes and not have the initial data set.
The solution to this is to use NSPersistentStoreCordinator’s migratePersistentStore:toURL:options:withType:error method. However I found the documentation somewhat lacking in clarity so here is the solution that now appears to be working for me.

There are some things I wanted to guarantee, which are probably not applicable to many so I’ll document them here:

- The application has gone through a few object model changes and I wanted to support people that may have skipped versions.
- The application’s data store location has changed on one occasion, when I moved the store out of Documents and into the Library directory due to adding iTunes document sharing.

Before we look at the code we’ll have a look at a rough outline of the procedure, in handy bullet points, step is 4 is the one that took some figuring out:

  1. Run everything following this asynchronously.
  2. Determine location of any existing stores, if not default to initial data set.
  3. Check to see if application supports iCloud, e.g. is running iOS 5.
  4. Configure store options if it does support iCloud.
  5. Lock the persistent store coordinator.
  6. If there is a cloud data set already, simply add it.
  7. If there isn’t DON’T add it, the migrate function adds it.
  8. Unlock persistent store coordinator.
  9. Send messages out so that view controllers that need to can redo their fetches.

A few caveats about the following, it’s not tested aside from ensuring it does migrate data into the store. I’ve not yet verified that the data then careers across the cloud to another device but I’ll update the post as soon as I do.

The code isn’t optimal, but that should be obvious! ;) The method should be called where in your code you’d normally add the store. A lot of the framework, minus the migrations stuff, is based on some example iCloud code.
Also note that the if block with the “//Migrate old store to new AND migrate into cloud” comment is devoid of code. In that block I would need to update the old store to match the new one and THEN migrate the store as I’m using my own migrations code which also allows importing of new data and a few other tweaks, which I think I’ve previously blogged.

CLOUD_NAME,STORE_NAME are #defines. DLog is a #define macro to NSLog that is blank in production environments.

- (void)aSynchronouslyAddPersistentStore {
  NSString *cloudPath = [[self applicationLibraryDirectory] stringByAppendingPathComponent:CLOUD_NAME];
  NSString *preCloudPath = [[self applicationLibraryDirectory] stringByAppendingPathComponent:STORE_NAME];
  NSString *defaultStorePath = [[NSBundle mainBundle] 
                                pathForResource:@"InitialData" ofType:@"sqlite"];
  NSString *oldStorePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:STORE_NAME];
  
  
  // do this asynchronously since if this is the first time this particular device is syncing with preexisting
  // iCloud content it may take a long long time to download
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *storeUrl = [NSURL fileURLWithPath:cloudPath];
    NSURL *migrateUrl = nil;
    
    /** 
     Find out which store has the data we need to migrate,
     if any.
     */
    if (![fileManager fileExistsAtPath:cloudPath]) {
      if ([fileManager fileExistsAtPath:preCloudPath]) {
        // Migrate old application data.
        migrateUrl = [NSURL fileURLWithPath:preCloudPath];
      } else if ([fileManager fileExistsAtPath:oldStorePath]) {
        // Migrate old store to new AND migrate into cloud.
      } else if ([fileManager fileExistsAtPath:defaultStorePath]) {
        // Migrate (copy) in initial recipe data set.
        migrateUrl = [NSURL fileURLWithPath:defaultStorePath];
      }
    }

    // this needs to match the entitlements and provisioning profile
    NSURL *cloudURL = nil;
    if([fileManager respondsToSelector:@selector(URLForUbiquityContainerIdentifier:)])
    {
      cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil];
      NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:@"chefsbook_v14b3"];
      cloudURL = [NSURL fileURLWithPath:coreDataCloudContent];
      DLog(@"cloudURL : %@", cloudURL);
    }
    
    //  The API to turn on Core Data iCloud support here.
    NSDictionary *options = nil;
    if (cloudURL) {
      options = [NSDictionary dictionaryWithObjectsAndKeys:
                 @"com.thelostsouls.chefsbook", NSPersistentStoreUbiquitousContentNameKey, 
                 cloudURL, NSPersistentStoreUbiquitousContentURLKey, 
                 [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, 
                 [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption
                 ,nil];
    } 
    
    NSError *error = nil;
    [persistentStoreCoordinator_ lock];
    DLog(@"Persistent Store ****LOCKED****");
    
    /**
     If I migrate url is set don't just add, migrate!
     Otherwise proceed as planned.
     */
    if (migrateUrl) {
      NSPersistentStore *srcPS = [persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType
                                                configuration:nil
                                                          URL:migrateUrl
                                                      options:nil
                                                        error:&error];
      if (![persistentStoreCoordinator_ migratePersistentStore:srcPS
                                                         toURL:storeUrl
                                                       options:options
                                                      withType:NSSQLiteStoreType 
                                                         error:&error]) {
        DLog(@"Error migrating data: %@, %@", error, [error userInfo]);
        abort();
      }
    }
    else
    {
      if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType 
                                                     configuration:nil 
                                                               URL:storeUrl 
                                                           options:options 
                                                             error:&error]) {
        DLog(@"Error adding persistent store: %@, %@", error, [error userInfo]);
        abort();
      }    
    }
    [persistentStoreCoordinator_ unlock];
    DLog(@"Persistent Store ****UNLOCKED****");
    
    // tell the UI on the main thread we finally added the store and then
    // post a custom notification to make your views do whatever they need to such as tell their
    // NSFetchedResultsController to -performFetch again now there is a real store
    dispatch_async(dispatch_get_main_queue(), ^{
      DLog(@"asynchronously added persistent store!");
      [[NSNotificationCenter defaultCenter] 
       postNotificationName:AppDelegateSharedPSCAddedStore
       object:self userInfo:nil];
    });
  });

}

So there you go.

Still a worry are scenarios where iCloud support is enabled/disabled and/or the data deleted from the cloud via the settings menu. Hopefully nobody would do that and leave iCloud enabled or they’d end up with an empty application and no recovery. That’s also the reason I’m still supporting and improving the code that saves the docs out to iTunes.

Another thought just occurred to me. The following scenario needs serious consideration:

  1. User starts app with iCloud support, initial data set loaded.
  2. User proceeds to add a handful of data.
  3. User installs Chef’s Book on second device with iCloud enabled for app.
  4. App starts and copies in default data PLUS receiving iCloud data.

Will it fail, magically know it’s the same (eminently probable), or duplicate data?

Time to find out!

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Well I found out, it hangs permanently. Thanks to some helpful people on the apple forums I know why. So the above code is only partially correct. What needs to be done is to also query data on the cloud using NSMetadataQuery and examine the results, if there is cloud data you should use that and not add a persistent store in the normal way (I guess?). What I now need to find out, having successfully been able to query the cloud and find LOTS of data on there from my previous tests, is how you should add that data.

Then I’m going to rewrite the entire core data start up code from the bottom up.

This entry was posted in Chef's Book, Programming and tagged , , , , , , . Bookmark the permalink.

15 Responses to Progress with adding iCloud to Chef’s Book.

  1. Steve Flack says:

    Good blog post. You seem to have got as far as me and hit the same obstacle – multiple devices and initial data sets. I’d be really keen on seeing your solution.

    • Diziet says:

      So would I. The solution will be posted here in a new post when I’ve found one. I have had a response on the main forums, which went along the lines of ‘File a bug.’ so it may be some time before a workable solution is discovered.

  2. Steve Flack says:

    Can you post a link to that discussion on the dev forums?

  3. Aaron Kardell says:

    Did you track down a solution for the problem you mentioned? It seems like you’ve got one of the best potential solutions out there, but I’m not sure yet how to deal with sync’ing two existing data sets.

    • Diziet says:

      Apologies for the delay in approving comments. I’ve been away for the last 3 weeks. And no… I’ve not yet come up with a solution but truth be told nor have I invested more than a few hours a week into it in recent times.

      Seeing as my paid job took up more of my time I refocused my time on tasks that also needed doing but were known factors such as searching recipes.

      I’ll update this post with more info the next time I give it a poke and prod. My conclusion was though that the technology simply isn’t ready for this usage.

  4. catinamosh says:

    Have you found a solution to this? I’m also on the same boat as you are.

    • Diziet says:

      Apologies for the delay in approving comments. I’ve been away for the last 3 weeks. Please see the reply to Aaron Kardell for more info.

  5. vivek says:

    hey what values are their in #define CLOUD_NAME and STORE_NAME

    • Diziet says:

      That takes me back a bit, I never got that code working properly and in general I would advise steering well clear of Core Data + iCloud syncing. It simply doesn’t work as originally promised and when you do get it working it’s very unstable and not a good user experience. Having said that I’ve not revisited the problem in about 1 1/2 years.

      To answer your question I defined STORE_NAME as the file name, with no path, of the sqlite file for Core Data. In Chef’s Books case it is “Chefs_Book.sqlite”. CLOUD_NAME was defined as Chefs_Book_iCloud.sqlite. CLOUD_KEY (if present in the posted code) was “chefsbook_v14b6″ which I think was part of some migration code. The idea here was, iirc, to migrate data.

      I hope that helps/clarifies, but again I never got it working and haven’t revisited it since.

  6. vivek says:

    Thanks, for your reply. can u please expand [self applicationLibraryDirectory] and [self applicationDocumentsDirectory].

    • Diziet says:

      They are small helper functions that return the Library or Document path for your application. They look something like this (formatting of this code will be bad but you should get the gist):

      - (NSString *)applicationLibraryDirectory {
      NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey];
      NSString *libraryDirectory = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
      NSUserDomainMask,
      YES)
      lastObject] stringByAppendingPathComponent:bundleID];
      NSFileManager *fileManager = [NSFileManager defaultManager];
      if (![fileManager fileExistsAtPath:libraryDirectory]) {
      [fileManager createDirectoryAtPath:libraryDirectory
      withIntermediateDirectories:YES
      attributes:nil
      error:NULL];
      }
      return libraryDirectory;
      }

      - (NSString *)applicationDocumentsDirectory {
      return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
      NSUserDomainMask,
      YES)
      lastObject];
      }

    • Diziet says:

      Oh worth noting that for application Library directory, it’s not quite *just* the library directory but also a directory for your app within that based on the Bundle ID. For example, with chefs book it would return:

      FullPathToAppInSimulatorOrDevice/Library/com.thelostsouls.chefsbook

  7. vivek says:

    Where to call this method excatly……?

    • Diziet says:

      Anywhere you feel appropriate for the design of your app. Give or take for me this was somewhere after or in `applicationDidFinishLaunching:withOptions:`

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>