Warning
This how-to is only relevant if your application is already using Core Data. If you need to simultaneously add Core Data and iCloud, you may have to adapt step 4 to fit your needs.
General explanations on how to use Core Data with iCloud are given here : Using Core Data with iCloud Release Notes.
1st step : Enable iCloud on provisioning profiles
First of all, you have to enable iCloud on your App Id on Apple Provisioning Portal. Go to iOS Dev Center > iOS Provisioning Portal > App IDs. Click on « Configure » for the App ID you want to modify, check “Enable for iCloud” and save your changes.
Then, always into provisioning portal, go to “Provisioning”, regenerate development and distribution provisioning profiles using the App ID you just changed, download them and add them to XCode.
2nd step : Enable iCloud at application level
When you stil are into provisioning portal, access to (top menu) Member Center > Your Account and carefully note the value of “Individual ID”.
Into XCode, select the root of your project, then “Summary” tab, and check “Enable Entitlements” at the bottom of the page. A new file named <project>.entitlements is created and added to the project's tree. Open this file. Values for keys “com.apple.developer.ubiquity-container-identifiers” and “com.apple.developer.ubiquity-kvstore-identifier” must look like “$(TeamIdentifierPrefix)<project bundle identifier>”. If it's not the case, fix them. If you want, you can replace “$(TeamIdentifierPrefix)” with your individual ID.
3rd step : Always useful macros
As iCloud is available starting iOS 5.0, it may be useful to have some macros to test against iOS version. You have to add to your project a new header file (i.e. “Macros.h”), and add to it the following content :
#define IOS_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame) #define IOS_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending) #define IOS_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) #define IOS_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) #define IOS_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)
4th step : Changes to AppDelegate
Now, to have to modify the AppDelegate to manage iCloud.
First of all, check your AppDelete has a method to save Core Data context (with old XCode projects, it's named “- (void)saveContext”). Add calls to this method to the following ones : “- (void)applicationWillResignActive:(UIApplication *)application”, “- (void)applicationDidEnterBackground:(UIApplication *)application”, “- (void)applicationWillEnterForeground:(UIApplication *)application” et “- (void)applicationWillTerminate:(UIApplication *)application”. As iCloud synchronisation will be triggered by Core Data context saves, you don't want to miss one.
Then you have to modify “- (NSManagedObjectContext *)managedObjectContext”:
- (NSManagedObjectContext *)managedObjectContext { if (__managedObjectContext != nil) { return __managedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { if (IOS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"5.0")) { 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; } else { __managedObjectContext = [[NSManagedObjectContext alloc] init]; [__managedObjectContext setPersistentStoreCoordinator:coordinator]; } } return __managedObjectContext; }
The section for iOS 5 is here to asynchronously manage context changes and iCloud notifications.
The you need to modify “- (NSPersistentStoreCoordinator *)persistentStoreCoordinator”:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (__persistentStoreCoordinator != nil) { return __persistentStoreCoordinator; } NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"<sqlite file>.sqlite"]; __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; NSPersistentStoreCoordinator* psc = __persistentStoreCoordinator; if (IOS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"5.0")) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSFileManager *fileManager = [NSFileManager defaultManager]; // Migrate datamodel NSDictionary *options = nil; // this needs to match the entitlements and provisioning profile NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:@"<individual ID>.<project bundle identifier>"]; NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:@"data"]; if ([coreDataCloudContent length] != 0) { // iCloud is available cloudURL = [NSURL fileURLWithPath:coreDataCloudContent]; options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, @"<app name>.store", NSPersistentStoreUbiquitousContentNameKey, cloudURL, NSPersistentStoreUbiquitousContentURLKey, nil]; } else { // iCloud is not available options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; } NSError *error = nil; [psc lock]; if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [psc unlock]; dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"asynchronously added persistent store!"); [[NSNotificationCenter defaultCenter] postNotificationName:@"RefetchAllDatabaseData" object:self userInfo:nil]; }); }); } else { NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; NSError *error = nil; if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } } return __persistentStoreCoordinator; }
Finally, you have to add method managing changes from iCloud:
- (void)mergeiCloudChanges:(NSNotification*)note forContext:(NSManagedObjectContext*)moc { [moc mergeChangesFromContextDidSaveNotification:note]; NSNotification* refreshNotification = [NSNotification notificationWithName:@"RefreshAllViews" object:self userInfo:[note userInfo]]; [[NSNotificationCenter defaultCenter] postNotification:refreshNotification]; } // NSNotifications are posted synchronously on the caller's thread // make sure to vector this back to the thread we want, in this case // the main thread for our views & controller - (void)mergeChangesFrom_iCloud:(NSNotification *)notification { NSManagedObjectContext* moc = [self managedObjectContext]; // this only works if you used NSMainQueueConcurrencyType // otherwise use a dispatch_async back to the main thread yourself [moc performBlock:^{ [self mergeiCloudChanges:notification forContext:moc]; }]; }
5th step : Changes to the controllers using Core Data context.
As iCloud changes are asynchronously received, you have to manage them with the help of the notifications created into the AppDelegate. It may be done into RootViewController (adapt to fit your needs).
First, update the “RootViewController.h” file to add the following property
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
and the following private block :
@private // because ivars should be private, and it is really important // that all code always goes through the accessor methods to ensure that these // are properly initialized. Without the funny __ then KVC might "help" us too much // With iCloud importing data asynchronously, there are more timing and multi-threading issues NSFetchedResultsController *fetchedResultsController__ ; NSManagedObjectContext *managedObjectContext__;
Then, update “RootViewController.m”
Add the following synthesize statements
@synthesize fetchedResultsController=__fetchedResultsController; @synthesize managedObjectContext=__managedObjectContext;
and add the following getter.
- (NSFetchedResultsController *)fetchedResultsController { if (__fetchedResultsController != nil) { return __fetchedResultsController; } /* Set up the fetched results controller. */ // Create the fetch request for the entity. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. NSEntityDescription *entity = [NSEntityDescription entityForName:@"<entity name>" inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. [fetchRequest setFetchBatchSize:20]; // Edit the sort key as appropriate. NSSortDescriptor *sortDescriptorName = [[NSSortDescriptor alloc] initWithKey:@"<sort key>" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptorName, nil]; [fetchRequest setSortDescriptors:sortDescriptors]; // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"<section name key path>" cacheName:@"<cache name>"]; aFetchedResultsController.delegate = self; self.fetchedResultsController = aFetchedResultsController; [aFetchedResultsController release]; [fetchRequest release]; [sortDescriptorName release]; [sortDescriptors release]; NSError *error = nil; if (![self.fetchedResultsController performFetch:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } return __fetchedResultsController; }
Then, add the following method.
// because the app delegate now loads the NSPersistentStore into the NSPersistentStoreCoordinator asynchronously // we will see the NSManagedObjectContext set up before any persistent stores are registered // we will need to fetch again after the persistent store is loaded - (void)reloadFetchedResults:(NSNotification*)note { NSError *error = nil; if (![[self fetchedResultsController] performFetch:&error]) { /* Replace this implementation with code to handle the error appropriately. abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button. */ NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } if (note) { [self.tableView reloadData]; } }
You have to modify “- (void)viewDidLoad” in order to add a notification observer (raised by the AppDelegate) which run the above method:
- (void)viewDidLoad { [super viewDidLoad]; ... your code... // observe the app delegate telling us when it's finished asynchronously setting up the persistent store [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadFetchedResults:) name:@"RefetchAllDatabaseData" object:[[UIApplication sharedApplication] delegate]]; }
Finally, you have to remove the observer when unloading the view.
- (void)viewDidUnload { [super viewDidUnload]; // Relinquish ownership of anything that can be recreated in viewDidLoad or on demand. // For example: self.myOutlet = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; }




Twitter
Facebook
Google+
LinkedIn
Commentaires
Hmm.. so where does the fetchedResultsController come from in the reloadFetchedResults?
dwildayYou're right, I forgot this part. I've just updated the post, check for the beginning of part 5.
Arnaud BoudouGreat blog entry however I still must be doing something wrong.
I keep getting this error
The entitlements specified in your application’s Code Signing Entitlements file do not match those specified in your provisioning profile.
Could you take a snap shots of the summary screen and anything else which might be useful.
rodgerI've added some screenshots. I hope it will help you.
Arnaud Boudou@rodger
I had a similar issue. They way I solved it I've documented in a comment over here:
http://www.raywenderlich.com/forums...
hope that helps.
dwilday@Arnaud
I've noticed that when I add an observer for "RefetchAllDatabaseData" I get an error retrieving the data. When I instead listen for "RefreshAllViews" it works....but just takes a very very long time (30seconds to multiple minutes). It is also called many many times.
Is there any way to tell if icloud data is being loaded or if I need to create new data?
dwildayMy project at it's root displays a list of user generated items. IF there are no items in the list I display instructions on how to begin. I'd like to modify this with a "wait" message if that's what's necessary.
The only way to tell if new iCloud data is available is too listen to "RefetchAllDatabaseData" and "RefreshAllViews" notifications.
The first one is mainly used when the distant persistentStore is available, to force the reload of the local store after sync with iCloud (i.e. reload your tableData).
The second notifications is used to handle individual changes on a single record (i.e. reload the detailView)
About times, you'll be depend on the way iCloud manage its notifications on changes, I'm afraid you can't do anything about it.
Arnaud BoudouThanks for the info... and the great tutorial!
Somehow my project no longer connects (while it did just fine yesterday) so at this point I'd be happy to even have that delay. :) Oh well.. back to hitting my head against the wall.
dwildayYou're welcome :)
Core Data with iCloud is a bit buggy with iOS 5.0 : one of my app is unable to connect to iCloud since I've deleted then reinstalled it. And I'm not the one with this issue. I hope iOS 5.0.1 will fix this issue.
Arnaud BoudouOutstanding how-to. This is the first comprehensive implementation of this I have found and it was immensely helpful to me. Thanks a bunch for sharing.
Dean DavidsTo share my own experience, I had trouble at first, my app crashed before completing the process. The console reported that "this persistent store could not save data". I put a breakpoint in the persistentStoreCoordinator method to see what was happening.
Surprisingly, when I stepped through line by line, it succeeded.
After a bit of consideration and some trial, I found the culprit to be a method that I had checking for entries and filling with some sample data if none found. I commented it out and that solved my issue. Now to find a more appropriate point at which to make that call.
Thanks again, nice job.
I see that iOS 5.0.1 was just released into the wild.
dwildayAny idea if this actually solved the CoreData iCloud issues? I'm going to start tinkering with it again myself when I get some free time in the office.
Hi. I have no idea yet. I won't have time to test it until Monday. When I have news, I'll leave a comment here.
Arnaud BoudouThanks!
dwildayIt'll probably not be until Wednesday or Thursday when I get a chance to work with it. I'll post something if I happen to notice better behavior.
I've just tried with iOS 5.0.1, and I still have the same issue.
Arnaud BoudouGreat tutorial :) However i've stucked on adding @private code to my RootViewController. Can anybody explain how to do it..?
Vkas@interface RootViewController : UITableViewController
@private
NSFetchedResultsController *fetchedResultsController__ ;
NSManagedObjectContext *managedObjectContext__;
//properties + methods
@end
Upper code don't work (error:Illegal interface qualifier) as any other options. Sorry for noob question but i've never seen @private command in obj-C. Thanks for reply.
Fixed above error by taking private code into brackets {@private ...}.
However i'm facing new problem..
"NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"<section name key path>" cacheName:@"<cache name>"];
aFetchedResultsController.delegate = self;"
VkasCan you provide any example of these <section name key path> and <cache name>..? Do i have to set this cache name somewhere else too..?
Also i get warning in setting delegate to self (warning: Semantic Issue: Passing 'RootViewController *const __strong' to parameter of incompatible type 'id<NSFetchedResultsControllerDelegate>')
<section name key path> : it's the name of the key path of the NSManagedObject you want to use as section title (if you don't want to use sections into your table view, you may use nil value)
<cache name> : use the name you want at your discretion.
About delegate issue, does your RootViewController uses protocol NSFetchedResultsControllerDelegate ? Interface declaration should be
@interface RootViewController : UITableViewController <any_needed_protocols, …, NSFetchedResultsControllerDelegate> {
Arnaud BoudouI entered the code from above into the stock Master-Detail Application with core data selected. Everything compiled fine. Then I installed on both iPhone 4 and iPad 2. After I add some time stamp events, I go to the manage storage in the iCloud section of the settings app and everything shows up where they are supposed to on both devices. The only problem is that the events do not show up on the screen of other device. If I add a time stamp to the iPhone, it does not show up on the iPad screen or visa versa. I triple checked to make sure that the code was right and not just a typo. Please advise.
gmazerI would like to encrypt the data on iCloud. I have done this without iCloud by including the code:
NSDictionary *fileAttributes = [NSDictionary
dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
if ((![NSFileManager defaultManager setAttributes:fileAttributes ofItemAtPath:
storeURL path error:&error])) {
NSLog(@"Unresolved error setting file protection attribute to database: %@",
error);}
at the end of the "(NSPersistentStoreCoordinator *)persistentStoreCoordinator" method. How do I enable encryption of the iCloud database?
Merci
Tom