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:
- Create a new App ID in the Provisioning Portal.
- Find the provisioning profile associated with the app. Click Edit->Modify, then change the App ID to the one you just created.
- Submit the change, then replace the existing profile in Xcode with the one you just created.
- Change all instances of
<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). - Restart Xcode since you changed information regarding provisioning profiles (it will complain otherwise and refuse to run on the device).
- Ensure that the new profile is on the devices you wish to install the app on, then build and run. Everything should work just fine at this point.