Crash when adding persistent store (iCloud enabled) in app delegate

justin picture justin · Nov 5, 2011 · Viewed 13.1k times · Source

I am going to start updating this to help those seeking to use this as reference for their own personal code.

Newest update

  • I'm fairly sure I have found a way to resync devices back together once they have stopped talking to each other. I'm going to update my answer below with all of the details. I thoroughly hope you all find this helpful. It's taken almost 2 months of trial and error to figure this one out. So please reference and share this with others who are having similar issues getting devices to once again talk to each other through iCloud. It took me forever to figure this all out, so I am more than happy to save as many other developers as possible from having to create their own make-shift fixes.

Another addition to help set up correctly

  • I found that after updating an app that has iCloud data associated with the account can cause a crash upon opening it because the iCloud data will attempt to merge immediately into the device (where the device has not yet set up its persistent store). I have now added @property (nonatomic, readwrite) BOOL unlocked; to AppDelegate.h and @synthesize unlocked; to AppDelegate.m. I then changed my - (NSPersistentStoreCoordinator *)persistentStoreCoordinator method as well as my - (void)mergeChangesFrom_iCloud method, both of which will be shown below (in the middle for the persistent store setup and at the bottom for the iCloud merge method). In essence, I am telling the app to prevent iCloud from merging data until the app has set up its persistent store. Otherwise, you will see the app crash due to unreadable faults.

Here is how I am setting up my persistentStoreCoordinator:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }


    // here is where you declare the persistent store is not prepared;
    self.unlocked = NO;

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Maintain_My_Car.sqlite"];

    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];   

    NSPersistentStoreCoordinator *psc = __persistentStoreCoordinator; 

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSDictionary *options = nil;

        NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil];

        NSString *coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:@"data"];

        if (coreDataCloudContent.length != 0) {
            // iCloud enabled;

            cloudURL = [NSURL fileURLWithPath:coreDataCloudContent];
            options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, @"<bundleIdentifier>.store", NSPersistentStoreUbiquitousContentNameKey, cloudURL, NSPersistentStoreUbiquitousContentURLKey, nil];

        } else {

            // iCloud not enabled;
            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(@"bad things %@ %@", error, [error userInfo]);
            abort();

        }
        [psc unlock];

        // the store is now prepared and ready for iCloud to import data;
        self.unlocked = YES;


        dispatch_async(dispatch_get_main_queue(), ^{

            NSLog(@"iCloud persistent store added");

            [[NSNotificationCenter defaultCenter] postNotificationName:@"RefetchAllDatabaseData" object:self userInfo:nil];

        });
    });

    return __persistentStoreCoordinator;
}

<myAppKey> and <bundleIdentifier> are actual values, of course. I am just masking them for the purpose of sharing this code.

I know that some people are still having troubles with this and may be using this question as reference on how to set up their own iCloud-enabled Core Data applications, so I want to update this whenever I make changes to my personal code, ensuring that all of you can use the code that works for me. In this update, I changed the initial cloudURL from [fileManager URLForUbiquityContainerIdentifier:@"<TeamIdentifier>.<bundleIdentifier>"] to [fileManager URLForUbiquityContainerIdentifier:nil], ensuring that the container information is gathered from the entitlements file.

Additional methods _notificationArray is defined as the following: @property (nonatomice, strong) NSMutableArray *notificationArray; @synthesize notificationArray = _notificationArray;

- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
    if (self.unlocked) {
        NSManagedObjectContext *moc = [self managedObjectContext];

        if (self.notificationArray.count != 0) {
            for (NSNotification *note in _notificationArray) {
                [moc performBlock:^{
                    [self mergeiCloudChanges:note forContext:moc];
                }];
            }
            [_notificationArray removeAllObjects];
            [moc performBlock:^{
                [self mergeiCloudChanges:notification forContext:moc];
            }];
        } else {
            [moc performBlock:^{
                [self mergeiCloudChanges:notification forContext:moc];
            }];
        }
    } else {
        if (_notificationArray == nil) {
            _notificationArray = [[NSMutableArray alloc] init];
        }
        [_notificationArray addObject:notification];
    }
}

- (void)resetStore {
    [self saveContext];
    __persistentStoreCoordinator = nil;
    __managedObjectContext = nil;
    // reset the managedObjectContext for your program as you would in application:didFinishLaunchingWithOptions:
    myMainView.managedObjectContext = [self managedObjectContext];
    // the example above will rebuild the MOC and PSC for you with the new parameters in mind;
}

Then there is the mergeiCloudChanges:forContext: method:

- (void)mergeiCloudChanges:(NSNotification *)note forContext:(NSManagedObjectContext *)moc {
    // below are a few logs you can run to see what is being done and when;
    NSLog(@"insert %@", [[note userInfo] valueForKey:@"inserted"]);
    NSLog(@"delete %@", [[note userInfo] valueForKey:@"deleted"]);
    NSLog(@"update %@", [[note userInfo] valueForKey:@"updated"]);
    [moc mergeChangesFromContextDidSaveNotification:note];

    NSNotification *refreshNotification = [NSNotification notificationWithName:@"RefreshAllViews" object:self userInfo:[note userInfo]];
    [[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
    // do any additional work here;
}

Initial problem

  • Using iCloud on iOS 5.0.1, I'm occasionally getting errors pertaining to the persistent store. I'm going to continue updating this with new information as I find it through experimenting, but so far the solution I provided is the only way I can get the app working properly again (unfortunately jlstrecker's solution didn't work for me) once I start seeing the error, which is the following:

    -NSPersistentStoreCoordinator addPersistentStoreWithType:configuration:URL:options:error:: CoreData: Ubiquity: Error attempting to read ubiquity root url: file://localhost/private/var/mobile/Library/Mobile%20Documents/./data/. Error: Error Domain=LibrarianErrorDomain Code=1 "The operation couldn’t be completed. (LibrarianErrorDomain error 1 - Unable to initiate item download.)" UserInfo=0x176000 {NSURL=file://localhost/private/var/mobile/Library/Mobile%20Documents/./data/, NSDescription=Unable to initiate item download.}

    For the life of me, I cannot figure out why I'm seeing this all the sudden or how to make it stop. I have deleted the app from both devices, deleted the iCloud data which was previous syncing between them, and deleted any data from backups regarding the apps. I have restarted Xcode, restarted both devices, cleaned the Xcode project, yet nothing has stopped the error from showing up. I've never seen this error before and have had zero luck finding anything online on how to pin it down.

    The app crashes here:

    if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
    
        NSLog(@"bad things %@ %@", error, [error userInfo]);
        abort();
    
    }
    

    The log is never hit, nor is the abort. I just see the error above and the app itself becomes unresponsive. If anyone can help point me in the right direction, I would be very appreciative.

Previous issues/questions

  • This seems to continue even after the update from the beta to the public release of 5.0.1. The last time it happened to me was after changing my managed context data model. Considering I haven't released the app yet, I didn't bother merging a new version of the model. I just deleted and reinstalled the app on my devices, but then it refused to cooperate with the data stored in the iCloud container, by which I mean that I received an error that the store could not download items. I imagine this is due to conflicting data model types, which makes perfect sense. So it seems you just need to get rid of the data within the iCloud container without getting rid of the container. Deleting the iCloud data seems to kill everything off, in essence disabling the container and App ID. Since it seemed simpler, I tried creating a new container as suggested by jlstrecker, but unfortunately, this didn't help at all. So once again, I had to go through the steps I outlined in my answer, which again did the trick. But considering how annoying it is to have to create new App IDs and update provisioning profiles each time, I thought it best to update what I've learned to potentially narrow down the cause and get to a quicker solution.

    Going through iCloud > Storage & Backup > Manage Storage, then deleting the app would appear to be the best solution to empty the data, but doing this seems to corrupt the container, leading to the error above. And after successfully doing this, no matter how many times I delete the app and reinstall it to the device (to make it appear like it's the first time appearing on the device and hopefully recreate the container), I can never get the app to show in the Documents & Data list again. This is somewhat concerning if it means that anyone who deletes data from their iCloud like that means that iCloud will not work for the app ever again. I am only using a development profile on the app so far, so perhaps using a distribution profile might make some difference, but I will have to test that before saying anything for certain.

I hope these new updates help anyone who may be having trouble setting up their store. It has been working great for me so far. I will be sure to update more if I find better fixes or just anything that makes the process more seemless.

Answer

justin picture justin · Nov 7, 2011

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:

  1. Create a new App ID in the Provisioning Portal.
  2. Find the provisioning profile associated with the app. Click Edit->Modify, then change the App ID to the one you just created.
  3. Submit the change, then replace the existing profile in Xcode with the one you just created.
  4. 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).
  5. Restart Xcode since you changed information regarding provisioning profiles (it will complain otherwise and refuse to run on the device).
  6. 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.