Search code examples
objective-ccore-datansmanagedobjectnsmanagedobjectcontextcloudkit

Core Data save and Concurrency problems in nested loops + CloudKit


I'm using CloudKit to download an array of records (contained in myArray) The myArray enumeration is within the completion handler of the CloudKit block. There are a few nested CloudKit queries and array enumerations (example below). From there, I'm creating managed objects in a loop, and saving them, which will run only on first launch and then I'd expect Core Data to have them available persistently, so the app is designed to retrieve them without the need of re-creating them.

The problem is that my objects do not appear to save, as on the apps second launch the views are empty (or that the app saves some, or it crashes), and will only fill if I re-run the code to create the objects.

I think the issue may to do with concurrency issues / threads + Core Data - which seems to be the case after adding the compiler flag suggested. Consequently, I edited my code to make use of private queues for the NSManagedObjectContext, but still have crashes. I've edited the example below to show what my code looks like now.

I've simplified the code below for clarify and the purpose of the example, but it is more or less what I have:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  

//Download records from CloudKit, the following enumeration is within the CloudKit completion handler    

          [myArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
              MyManagedObj *managedObjToInsert = [NSEntityDescription
                    insertNewObjectForEntityForName:@"entityName" 
                    inManagedObjectContext:self.managedObjectContext];
              managedObjToInsert.property = obj[@"property"];

          //Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query

             [myNextArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
              MyManagedObj *nextManagedObjToInsert = [NSEntityDescription
                    insertNewObjectForEntityForName:@"entityName" 
                    inManagedObjectContext:self.managedObjectContext];
              nextManagedObjToInsert.property = obj[@"property"];
               nextManagedObjToInsert.relatedObj = managedObjToInsert; //relational

               }];

          }];

           NSError *error;

            if (![self.managedObjectContext save:&error])
            {
                NSLog(@"Problem saving: %@", [error localizedDescription]);
            }
    }

I've added the flag suggested in the answers below, and it seems like my managedobjectcontext is being passed outside the main thread, giving unpredictable results. Where do or how do I use the private queue blocks - assuming that is the solution to the problem?


Solution

  • Solved this issue using private queues with the help of the following documentation (in addition to the helpful comments/answers shared before this answer):

    The problem was that I was trying to save to the NSManagedObjectContext on the main thread whilst the code being executed by the cloudkit query to the database was occuring on another thread, resulting in crashes and inconsistent saves to the persistent store. The solution was to use the NSPrivateQueueConcurrencyType by declaring a new child context (this is only supported in iOS 5+).

    Working code now looks like*:

    //create a child context
    NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [privateContext setParentContext:[self managedObjectContext]];
    
    //Download records from CloudKit by performing a query
    
    [publicDatabase performQuery:myQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * resultsArray, NSError * error) {
    
        [resultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
    
            __block NSManagedObjectID *myObjID;
    
            //Async and not in main thread so requires a private queue
    
            [privateContext performBlockAndWait:^{
    
                MyManagedObj *managedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:@"entityOne" inManagedObjectContext:privateContext];
                managedObjToInsert.property = obj[@"property"];
                myObjID = managedObjToInsert.objectID;
    
                NSError *error;
                if (![privateContext save:&error]) //propergates to the parent context
                {
                    NSLog(@"Problem saving: %@", [error localizedDescription]);
    
                }
            }];
    
            //Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
    
            [publicDatabase performQuery:mySecondQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * secondResultsArray, NSError * error) {
    
                [secondResultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
    
                    [privateContext performBlockAndWait:^{
    
                        MyManagedObj *nextManagedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:@"entityTwo" inManagedObjectContext:privateContext];
                        nextManagedObjToInsert.property = obj[@"property"];
    
                        NSManagedObject *relatedObject = [privateContext objectWithID:myObjID];
                        nextManagedObjToInsert.relatedObj = relatedObject; //relational
                    }];
    
                }];
    
            }];
    
            NSError *childError = nil;
            if ([privateContext save:&childError]) { //propagates to the parent context
                [self.managedObjectContext performBlock:^{
                    NSError *parentError = nil;
                    if (![self.managedObjectContext save:&parentError]) { //saves to the persistent store
                        NSLog(@"Error saving parent %@", parentError);
                    }
                }];
            } else {
                NSLog(@"Error saving child %@", childError);
            }
    
        }];
    
    }];
    

    *please note that this is just an example to show how I solved the problem - there may be certain variable declarations missing, but the gist of the solution is there.