I am going to start updating this to help those seeking to use this as reference for their own personal code.
Newest update
Another clarification: A similar situation happened to me testing devices on iOS 6.0.1 and the 6.1 beta 2.
It is not completely fixed in iOS 5.1 as stated by @Slev. One device would completely freeze for roughly 80 seconds while trying to access the persistent store on iCloud but would never actually access the information stored there.
I believe this is due to a corrupted log file in the device's OS. Deleting the app or iCloud data on the device did nothing to fix the freeze/inability to access the iCloud store.
The one fix I found was to reset all settings
(erase all content works too) on the device. settings->general->reset.
Only then could I access the data on iCloud with my app on that device again. I hope this helps anyone else that came here looking for a solution to a very frustrating bug.
I got this error while using one device on 5.0.1 beta 1 and one on 5.0.0.
I got rid of the error by changing the name of the iCloud Container. In the iCloud Containers list, the first one has to match your app ID, but you can add additional containers with different names.
(Like slev's solution of changing the app ID, this is only a good solution if the app hasn't been released yet.)
Excuse me Slev, I've got the same problem that you have when you say that the app is crashing because the iCloud data will attempt to merge immediately into the device (where the device has not yet set up its persistent store) but I can't understand how have you solved this.
Your code is :
- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
if (self.unlocked) {
NSManagedObjectContext *moc = [self managedObjectContext];
[moc performBlock:^{
[self mergeiCloudChanges:notification forContext:moc];
}];
}
}
I haven't tried it yet but looking at this code I wonder what happen to the notifications that arrive when "unlocked" is false. Would you loose them?
Wouldn't it be better to have a while loop that test the "unlocked" property and spend some timespan until the property becomes true?
I hope you will understand my very bad english... :)
Thank you
Dave
UPDATE:
Everyone should really take a look at the iCloud Core Data session 227 from WWDC 2012. The source code they provide is an excellent starting point for an iCloud based solution. Really take the time to go through what they are doing. You will need to fill in some holes like copying objects from one store to another and de-duping. That being said, I am no longer using the migratePersistentStore
approach as described below for moving between local and iCloud stores.
My original answer:
Slev asked me to post some code for migrating a store from a local copy to iCloud and back again. This code is experimental and should not be used in production. It is only provided here as reference for us to share and move forward. Once Apple releases a proper reference application, you should probably consult that for your patterns and practices.
-(void) onChangeiCloudSync
{
YourAppDelegate* appDelegate = (YourAppDelegate*) [[UIApplication sharedApplication] delegate];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([iCloudUtility iCloudEnabled])
{
NSURL *storeUrl = [[appDelegate applicationDocumentsDirectory] URLByAppendingPathComponent:@"YourApp2.sqlite"];
NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil];
NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:@"data"];
cloudURL = [NSURL fileURLWithPath:coreDataCloudContent];
// The API to turn on Core Data iCloud support here.
NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:@"com.yourcompany.yourapp.coredata", NSPersistentStoreUbiquitousContentNameKey, cloudURL, NSPersistentStoreUbiquitousContentURLKey, [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,nil];
NSPersistentStore* store = [appDelegate.persistentStoreCoordinator.persistentStores objectAtIndex:0];
NSError* error;
if (![appDelegate.persistentStoreCoordinator migratePersistentStore:store toURL:storeUrl options:options withType:NSSQLiteStoreType error:&error])
{
NSLog(@"Error migrating data: %@, %@", error, [error userInfo]);
//abort();
}
[fileManager removeItemAtURL:[[appDelegate applicationDocumentsDirectory] URLByAppendingPathComponent:@"YourApp.sqlite"] error:nil];
[appDelegate resetStore];
}
else
{
NSURL *storeUrl = [[appDelegate applicationDocumentsDirectory] URLByAppendingPathComponent:@"YourApp.sqlite"];
// The API to turn on Core Data iCloud support here.
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];
NSPersistentStore* store = [appDelegate.persistentStoreCoordinator.persistentStores objectAtIndex:0];
NSError* error;
if (![appDelegate.persistentStoreCoordinator migratePersistentStore:store toURL:storeUrl options:options withType:NSSQLiteStoreType error:&error])
{
NSLog(@"Error migrating data: %@, %@", error, [error userInfo]);
//abort();
}
[fileManager removeItemAtURL:[[appDelegate applicationDocumentsDirectory] URLByAppendingPathComponent:@"YourApp2.sqlite"] error:nil];
[appDelegate resetStore];
}
}
Updated answer to resync your devices Months of tinkering around have led me to figuring out what (I believe) the rooted problem is. The issue has been getting devices to once again talk to each other after they fall out of sync. I can't say for sure what causes this, but my suspicion is that the transaction log becomes corrupted, or (more likely) the container of the log itself is recreated. This would be like device A posting changes to container A and device B doing the same as opposed to both posting to container C, where they can read/write to the logs.
Now that we know the problem, it's a matter of creating a solution. More tinkering led me to the following. I have a method called resetiCloudSync:(BOOL)isSource
, which is a modified version of the method above in my original question.
- (void)resetiCloudSync:(BOOL)isSource {
NSLog(@"reset sync source %d", isSource);
NSManagedObjectContext *moc = self.managedObjectContext;
if (isSource) {
// remove data from app's cloud account, then repopulate with copy of existing data;
// find your log transaction container;
NSURL *cloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSString *coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:@"store"];
cloudURL = [NSURL fileURLWithPath:coreDataCloudContent];
NSError *error = nil;
// remove the old log transaction container and it's logs;
[[NSFileManager defaultManager] removeItemAtURL:cloudURL error:&error];
// rebuild the container to insert the "new" data into;
if ([[NSFileManager defaultManager] createFileAtPath:coreDataCloudContent contents:nil attributes:nil]) {
// this will differ for everyone else. here i set up an array that stores the core data objects that are to-many relationships;
NSArray *keyArray = [NSArray arrayWithObjects:@"addedFields", @"mileages", @"parts", @"repairEvents", nil];
// create a request to temporarily store the objects you need to replicate;
// my heirarchy starts with vehicles as parent entities with many attributes and relationships (both to-one and to-many);
// as this format is a mix of just about everything, it works great for example purposes;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Vehicle" inManagedObjectContext:moc];
[request setEntity:entity];
NSError *error = nil;
NSArray *vehicles = [moc executeFetchRequest:request error:&error];
for (NSManagedObject *object in vehicles) {
NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:object.entity.name inManagedObjectContext:moc];
// check regular values;
for (NSString *key in object.entity.attributesByName.allKeys) {
[newObject setValue:[object valueForKey:key] forKey:key];
}
// check relationships;
NSMutableSet *relSet = [[NSMutableSet alloc] init];
for (NSString *key in object.entity.relationshipsByName.allKeys) {
[relSet removeAllObjects];
// check to see relationship exists;
if ([object valueForKey:key] != nil) {
// check to see if relationship is to-many;
if ([keyArray containsObject:key]) {
for (NSManagedObject *toManyObject in [object valueForKey:key]) {
[relSet addObject:toManyObject];
}
} else {
[relSet addObject:[object valueForKey:key]];
}
// cycle through objects;
for (NSManagedObject *subObject in relSet) {
NSManagedObject *newSubObject = [NSEntityDescription insertNewObjectForEntityForName:subObject.entity.name inManagedObjectContext:moc];
// check sub values;
for (NSString *subKey in subObject.entity.attributesByName.allKeys) {
NSLog(@"subkey %@", subKey);
[newSubObject setValue:[subObject valueForKey:subKey] forKey:subKey];
}
// check sub relationships;
for (NSString *subRel in subObject.entity.relationshipsByName.allKeys) {
NSLog(@"sub relationship %@", subRel);
// set up any additional checks if necessary;
[newSubObject setValue:newObject forKey:subRel];
}
}
}
}
[moc deleteObject:object];
}
[self resetStore];
}
} else {
// here we remove all data from the current device to populate with data pushed to cloud from other device;
for (NSManagedObject *object in moc.registeredObjects) {
[moc deleteObject:object];
}
}
[[[UIAlertView alloc] initWithTitle:@"Sync has been reset" message:nil delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil] show];
}
In this code, I have two distinct paths to take. One is for devices which are not in sync and need to have data imported from the source device. All that path does is clear the memory to prepare it for the data that is supposed to be in it.
The other (isSource = YES
) path, does a number of things. In general, it removes the corrupted container. It then creates a new container (for the logs to have a place to reside). Finally, it searches through the parent entities and copies them. What this does is repopulate the transaction log container with the information that is supposed to be there. Then you need to remove the original entities so you don't have duplicates. Finally, reset the persistent store to "refresh" the app's core data and update all the views and fetchedResultsControllers
.
I can attest that this works wonderfully. I've cleared the data from devices (isSource = NO
) who have not talked to the primary device (where the data is held) for months. I then pushed the data from the primary device and delightfully watched as ALL my data appeared within seconds.
Again, please feel free to reference and share this to any and all who have had problems syncing with iCloud.
Answer to original question, which is no longer affected after iOS 5.1 came out, which fixed the crash after removing your app's iCloud storage in your Settings
After many many many hours of trying anything and everything to get this sorted out, I tried creating a new App ID, updated the app's associated provisioning profile, changed around the iCloud container fields to match the new profile, and everything works again. I still have no idea why this happened, but it seems like the iCloud storage associated with that App ID got corrupted?
So bottom line is if this happens to anyone else, follow these steps and you should be good:
<bundleIdentifier>
to fit the new App ID (these would be in your main app Summary page, the entitlements for iCloud Containers and iCloud Key-Value Store, and in your AppDelegate file where you are creating the persistent store, like in my code above).