RSS

Core Data in iCloud

03 Apr

Introduction

The aim of this tutorial is to show how to add iCloud support to a project.  The starting point will be the final project from the end of the Core Data Universal / iPad Storyboard tutorial. I would recommend going through that tutorial first so you’re familiar with the Staff Manager project.

Prerequisites

Download the final project from the end of the Core Data Universal / iPad Storyboard tutorial. Extract and open it with Xcode.
Note: iCloud does NOT work in the iOS Simulator. I recommend testing on two devices, such as an iPad and an iPhone at the same time.

Create iCloud Staff Manager App ID

Head over to https://developer.apple.com/membercenter/ then enter the iOS Provisioning Portal.  The link looks like this:

Select App IDs

Select New App ID

Configure the App ID as follows then click Submit:

Click Configure:

Tick Enable for iCloud then click Done:

iCloud is now enabled for Staff Manager:

You will notice that the App ID has a prefix of the Team ID.  You will have a different Team ID prefix to me.  You will need to substitute your own prefix whenever you see my prefix in code:


To use the new App ID you’ll need a new Provisioning Profile so click Provisioning then New Profile:

Configure the Provisioning Profile as follows:

Click Submit then download your new profile.  If you can’t see the Download button refresh the page:

Once you have downloaded the provisioning profile file double click it which will automatically import it into Xcode:

In Xcode, configure the Staff Manager Code Signing Identity to use the new Provisioning Profile:

Enable Staff Manager Entitlements

To use the iCloud App ID we’ve just created we need to enable some Entitlements. Select the Staff Manager Targets:

Scroll to the Entitlements section (down the bottom) then configure it as follows:

Configure Core Data for iCloud

Currently the Persistent Store (StaffManager.sqlite) is stored on the local device. Once configured for iCloud it will still reside locally yet in a special iCloud enabled directory.

After poring through many approaches for enabling iCloud + Core Data I’ve come up with some succinct code used to create the persistent store. For re-usability and clarity the following variables are used:

  • iCloudEnabledAppID is the full App ID (including the Team Prefix). You will need to change this to match the Team Prefix found in your own iOS Provisioning Portal.
  • dataFile is the name of the SQLite database store file.
  • iCloud is the URL to your apps iCloud root path.
  • dataDirectory is the name of the directory the database will be stored in. It should always end with .nosync
  • logsDirectory is the name of the directory the database change logs will be stored in.
  • iCloudData = iCloud + dataDirectory
  • iCloudLogs = iCloud + logsDirectory

The upcoming code is responsible for returning a NSPersistentStoreCoordinator. Most of it is for setting what/where data and logs will be stored. The options NSMutableDictionary is used to set some new important options:

  • NSPersistentStoreUbiquitousContentNameKey will become the iCloud root (based on App ID)
  • NSPersistentStoreUbiquitousContentURLKey will become the change logs area.

Delete everything within the persistentStoreCoordinator method of AppDelegate.m then paste in the following code:

    if((__persistentStoreCoordinator != nil)) {
        return __persistentStoreCoordinator;
    }
 
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];    
    NSPersistentStoreCoordinator *psc = __persistentStoreCoordinator;
 
    // Set up iCloud in another thread:
 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // ** Note: if you adapt this code for your own use, you MUST change this variable:
        NSString *iCloudEnabledAppID = @"5KFJ75859U.Tim-Roadley.Staff-Manager";
 
        // ** Note: if you adapt this code for your own use, you should change this variable:        
        NSString *dataFileName = @"StaffManager.sqlite";
 
        // ** Note: For basic usage you shouldn't need to change anything else
 
        NSString *iCloudDataDirectoryName = @"Data.nosync";
        NSString *iCloudLogsDirectoryName = @"Logs";
        NSFileManager *fileManager = [NSFileManager defaultManager];        
        NSURL *localStore = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:dataFileName];
        NSURL *iCloud = [fileManager URLForUbiquityContainerIdentifier:nil];
 
        if (iCloud) {
 
            NSLog(@"iCloud is working");
 
            NSURL *iCloudLogsPath = [NSURL fileURLWithPath:[[iCloud path] stringByAppendingPathComponent:iCloudLogsDirectoryName]];
 
            NSLog(@"iCloudEnabledAppID = %@",iCloudEnabledAppID);
            NSLog(@"dataFileName = %@", dataFileName); 
            NSLog(@"iCloudDataDirectoryName = %@", iCloudDataDirectoryName);
            NSLog(@"iCloudLogsDirectoryName = %@", iCloudLogsDirectoryName);  
            NSLog(@"iCloud = %@", iCloud);
            NSLog(@"iCloudLogsPath = %@", iCloudLogsPath);
 
            if([fileManager fileExistsAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]] == NO) {
                NSError *fileSystemError;
                [fileManager createDirectoryAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName] 
                       withIntermediateDirectories:YES 
                                        attributes:nil 
                                             error:&fileSystemError];
                if(fileSystemError != nil) {
                    NSLog(@"Error creating database directory %@", fileSystemError);
                }
            }
 
            NSString *iCloudData = [[[iCloud path] 
                                     stringByAppendingPathComponent:iCloudDataDirectoryName] 
                                    stringByAppendingPathComponent:dataFileName];
 
            NSLog(@"iCloudData = %@", iCloudData);
 
            NSMutableDictionary *options = [NSMutableDictionary dictionary];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
            [options setObject:iCloudEnabledAppID            forKey:NSPersistentStoreUbiquitousContentNameKey];
            [options setObject:iCloudLogsPath                forKey:NSPersistentStoreUbiquitousContentURLKey];
 
            [psc lock];
 
            [psc addPersistentStoreWithType:NSSQLiteStoreType 
                              configuration:nil 
                                        URL:[NSURL fileURLWithPath:iCloudData] 
                                    options:options 
                                      error:nil];
 
            [psc unlock];
        }
        else {
            NSLog(@"iCloud is NOT working - using a local store");
            NSMutableDictionary *options = [NSMutableDictionary dictionary];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
 
            [psc lock];
 
            [psc addPersistentStoreWithType:NSSQLiteStoreType 
                              configuration:nil 
                                        URL:localStore 
                                    options:options 
                                      error:nil];
            [psc unlock];
 
        }
 
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:@"SomethingChanged" object:self userInfo:nil];
        });
    });
 
    return __persistentStoreCoordinator;

Comment out the following code found in the didFinishLaunchingWithOptions method of AppDelegate.m :

    /*
    [self setupFetchedResultsController];
 
    if (![[self.fetchedResultsController fetchedObjects] count] > 0 ) {
        NSLog(@"!!!!! ~~> There's nothing in the database so defaults will be inserted");
        [self importCoreDataDefaultRoles];
    }
    else {
        NSLog(@"There's stuff in the database so skipping the import of default data");
    }
    */

Configure Notifications

When underlying data changes unfortunately the table views have no idea. To fix this we need to get them to watch for stuff changing notifications. First we need to configure the managedObjectContext so replace the managedObjectContext method in AppDelegate.m with the following two methods:

- (NSManagedObjectContext *)managedObjectContext {
 
    if (__managedObjectContext != nil) {
        return __managedObjectContext;
    }
 
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
 
    if (coordinator != nil) {
        NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
 
        [moc performBlockAndWait:^{
            [moc setPersistentStoreCoordinator: coordinator];
            [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(mergeChangesFrom_iCloud:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:coordinator];
        }];
        __managedObjectContext = moc;
    }
 
    return __managedObjectContext;
}
 
- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
 
	NSLog(@"Merging in changes from iCloud...");
 
    NSManagedObjectContext* moc = [self managedObjectContext];
 
    [moc performBlock:^{
 
        [moc mergeChangesFromContextDidSaveNotification:notification]; 
 
        NSNotification* refreshNotification = [NSNotification notificationWithName:@"SomethingChanged"
                                                                            object:self
                                                                          userInfo:[notification userInfo]];
 
        [[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
    }];
}

That code tells the Managed Object Context to post notifications when iCloud updates data. To use those notifications add the following method to RolesTVC.m and PersonsTVC.m:

- (void)reloadFetchedResults:(NSNotification*)note {
    NSLog(@"Underlying data changed ... refreshing!");
    [self performFetch];
}

Also add the following to the end of the viewDidLoad method of RolesTVC.m and PersonsTVC.m:

    // Refresh this view whenever data changes
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(reloadFetchedResults:)
                                                 name:@"SomethingChanged"
                                               object:[[UIApplication sharedApplication] delegate]];

Finally, add the following method to RolesTVC.m and PersonsTVC.m which cleans up a little:

- (void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

That’s it!

Run the app on the iPad and iPhone at the same time. You should see changes made on one device reflected on the other pretty quickly.

There are a couple of things I’m not entirely happy with just yet and will work to resolve soon. These are:

  • Saving every keystroke for the purpose of updating the master split view isn’t fast.
  • Importing default data.

Hopefully you have enough information to get you started with iCloud anyway.

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer or making a small donation. I’m saving for a Retina Tutorial iPad!!

-Tim

Go to the Tutorials Index


Be Sociable, Share!
 

About Tim Roadley

I'm a 30 something year old married-with-children Infrastructure Manager living in Australia and working at Cuscal. I love iOS coding and have found the most effective way to learn (and retain) is by writing tutorials. I love getting feedback and helping like minded people learn iOS too :)
84 Comments

Posted by on April 3, 2012 in iOS Tutorials

 

84 Responses to Core Data in iCloud

  1. John in Denver

    April 20, 2013 at 11:11 pm

    This is an excellent starting point and we have it working in a production environment successfully with my data driven core data app. However, there is quite a bit of work that needs to be done to have and app that can make it into the store. Even using a device that is network and iCloud enabled there is an almost 100% chance of a crash the first time you run the app and it attempts to setup the iCloud, at best it will time out trying to sync which is also considered a crash. In subsequent attempts it will ultimately function, However the store testers will reject the app after the first crash of course.

    Anyway, as a starting point on initialization you need to check network and iCloud availability and respond accordingly, ask if the user wants to use iCloud the first time and show some sort of a status or progress indicator since it can take several minutes to sync iCloud the first time. These changes will eliminate the crashes and get the app approved. Next you need to consider how you handle keeping multiple devices in sync. Apple enumerates some examples, but provides no solutions.

    When I first got Tim’s code working my thought was, “this is way too easy” and I was correct!

     
    • Tim Roadley

      April 21, 2013 at 8:52 am

      John,

      I agree entirely!

      My book will cover the remaining parts to get iCloud stable, including:

      - Allowing the user to toggle their preference on using iCloud
      - Checking that a user is signed in and wants to use iCloud before loading the iCloud store
      - Handling iCloud account switches by maintaining an iCloud cache store per-user
      - Showing a loading dialogue while iCloud loads
      - Prevention of log file corruption using NSFilePresenter
      - De-duplication
      - Seeding data to iCloud (once)
      - Getting visibility of what’s in iCloud

      ..and a few more things!

      Cheers

       
  2. jc

    April 27, 2013 at 12:08 am

    Hi. I have to say it’s the most detailed tutorial I could find. Could you explain a few moments which are not quite clear?
    1. What is the purpose of lock of the persistentStoreCoordinator before performing ‘addPersistentStoreWithType:configuration:URL:options:error’ method?
    2. You place database store file depending on whether iCloud available or not. But what if user will activate iCloud later? In this case the app creates two different copies of store, right? Can we better place store file in some local store outside of iCloud container (applicationDocumentsDirectory) not relying on whether iCloud available or not, and just set LogsPath (if exists) during PersistentStoreCoordinator’s initialization?
    Thank you for advise.

     
    • Tim Roadley

      May 13, 2013 at 1:24 pm

      Hi JC, sorry for the late reply.

      1. You could get away without using the psc lock if everything is configured on the main thread.
      2. Handling complex scenarios such as a user changing their preference on using iCloud or switching accounts is covered in my book. It’s a quite a deep topic. iCloud is covered in 2 chapters (iCloud & Taming iCloud). The second of the two also covers seeding and deduplication strategies. The book will be released in October and the rough cut of the iCloud chapters will be available soon.

      Cheers

       

Leave a Reply