Avertissement

Cet how-to ne s'applique que pour les cas où votre application utilise déjà Core Data. Dans le cas où vous souhaitez ajouter Core Data et iCloud simultanément, vous allez devoir adapter l'étape 4 à vos besoins.

De plus, les explication générale sur l'utilisation de Core Data avec iCloud sont données ici : Using Core Data with iCloud Release Notes.

1ère étape : Activer iCloud au niveau des profils de provisionnement

Tout d'abord, vous devez activer iCloud au niveau de l'App ID sur le portail de provisionnement Apple. Pour celà, il faut accéder à iOS Dev Center > iOS Provisioning Portal > App IDs. Cliquez sur « Configure » pour l'App ID à modifier, cochez la case « Enable for iCloud » et sauvez les modifications.

Step 1

Ensuite, toujours dans le portail de provisionnement, aller à la partie « Provisioning », régénérez les profils de provisionnement de développement et de distribution concernés par l'App ID, téléchargez les et rajoutez les à XCode.

2ème étape : Activer iCloud au niveau de l'application

Tant que vous êtes dans le portail de provisionnement, accéder (menu du haut) à Member Center > Your Account et notez soigneusement la valeur de « Individual ID ».

Step 2 - Individual ID

Dans XCode, sélectionnez la racine de votre projet, puis l'onglet « Summary », et tout en bas cochez la case « Enable Entitlements ». Un fichier <projet>.entitlements est créé et ajouté à l'arborescence du projet. Ouvrez ce fichier. Les valeurs des clés « com.apple.developer.ubiquity-container-identifiers » et « com.apple.developer.ubiquity-kvstore-identifier » doivent être sous la forme « $(TeamIdentifierPrefix)<project bundle identifier> ». Si ce n'est pas le cas, corrigez les. Si vous le voulez, vous pouvez remplacer « $(TeamIdentifierPrefix) » par votre « individual ID ».

Step 2 - Summary tab

Step 2 - Entitlements file



3ème étape : Les macros toujours utiles

iCloud n'étant disponible que pour iOS 5.0 au minimum, il sera toujours utile d'avoir quelques macros pour tester la version du système. Pour cela, rajouter à votre projet un fichier d'entête (appelé par exemple « Macros.h »), et ajoutez-y le contenu suivant :

#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)

4ème étape : Modifications à apporter au AppDelegate

Maintenant, il faut modifier le AppDelegate pour y gérer iCloud.

Tout d'abord, vérifiez que le AppDelegate comporte une méthode chargée de sauvegarder le contexte Core Data (avec les vieux projets XCode, elle est ajoutée automatiquement et s'appelle « - (void)saveContext ». Ajoutez les appels à cette méthode dans les méthode suivante : « - (void)applicationWillResignActive:(UIApplication *)application », « - (void)applicationDidEnterBackground:(UIApplication *)application », « - (void)applicationWillEnterForeground:(UIApplication *)application » et « - (void)applicationWillTerminate:(UIApplication *)application ». Comme c'est la sauvegarde du contexte qui lance la synchro iCloud, autant ne pas lésiner sur ce point.

Ensuite, il faut modifier la méthode « - (NSManagedObjectContext *)managedObjectContext » comme suit

- (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;
}

La partie consacrée à iOS 5 est là pour gérer les modifications du contexte de manière asynchrone, et de gérer des notifications pour les évènements à iCloud.

Ensuite, il est nécessaire de modifier la méthode « - (NSPersistentStoreCoordinator *)persistentStoreCoordinator » comme suit :

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
 
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"<fichier sqlite>.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;
}

Enfin, il faut rajouter les méthode permettant de gérer les changements venant d'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];
    }];
}

5ème étape : Modifications à apporter aux contrôleurs utilisant le contexte Core Data

Finalement, les modifications venant d'iCloud étant reçues de manière asynchrone, il va vous falloir les traiter à l'aide des notifications créées dans le AppDelegate. Cela peut se faire par exemple dans le RootViewController (à adapter selon vos besoins).

D'abord, modifier le fichier « RootViewController.h » pour y ajouter la propriété suivante.

@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;

et le bloc « private » suivant :

@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__;

Ensuite, modifier le fichier « RootViewController.m »

Ajouter les « synthesize » suivants :

@synthesize fetchedResultsController=__fetchedResultsController;
@synthesize managedObjectContext=__managedObjectContext;

et le getter ci-dessous :

- (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;
}

Ensuite, rajouter la méthode suivante :

// 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];
    }
}

Il faut aussi modifier la méthode « - (void)viewDidLoad » pour y ajouter un observateur de notifications (lancées par le AppDelegate) et qui exécutera la méthode ci-dessus :

- (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]];
}

Et pour finir, il faut libérer l'observateur lors de la fermeture de la vue :

- (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];
}