I've been testing the new core data stack in iOS 10. My test app parses JSON data into core data and I am trying to make this happen while the user has access to the UI.
I am using the default core data stack and using a background context.
In AppDelegate.m:
- (NSPersistentContainer *)persistentContainer {
// The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
@synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"CoreDataTestingMDC"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}];
}
}
_persistentContainer.viewContext.automaticallyMergesChangesFromParent = YES;
return _persistentContainer;
}
I have a simple master-detail UI that shows the core data entities in the master view controller and detailed attributes in the detail view. If the user does not scroll the master view, everything works fine. If the user scrolls, I usually get this error on save:
Unresolved error Error Domain=NSCocoaErrorDomain Code=133020 "(null)" UserInfo={conflictList=(
"NSMergeConflict (0x600000667c00) for NSManagedObject (0x610000096490) with objectID '0xd000000000440000 <x-coredata://CFF27A51-8F9E-4898-A4EA-CD85C0AFF300/ContentItem/p17>'
with oldVersion = 44 and newVersion = 45...
It goes on to list the conflicting items which have exactly the same properties.
Also in my AppDelegate, I added a simple convenience method to generate the background context:
- (NSManagedObjectContext *)createBackgroundContext {
return [self.persistentContainer newBackgroundContext];
}
This is passed back to the AppDelegate for a save operation:
- (void)saveContext:(NSManagedObjectContext *) theContext {
NSError *error = nil;
if ([theContext hasChanges] && ![theContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}
The UI is running on the viewContext as expected. I have been very careful to use the background context for all JSON parser writing. No idea why this is crashing.
Update:
It appears that the error occurs whenever the app is run after an initial run. I can test it fine on a clean simulator or after deleting the app. It parses data into core data fine and will also update while the user is interacting with the app. On a second build and run, the app will crash with the above error.
It looks to me that you are having multiple writes taking place concurrently. To solve this you need write to core data in a single synchronous way.
In your core-data manager create a NSOperationQueue
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
[context performBlockAndWait:^{
blockCopy(context);
[context save:NULL]; //Don't just pass NULL here. look at the error and log it to your analytics service
}];
}]];
}
When you call enqueueCoreDataBlock
the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext
that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext
or with performBackgroundTask
) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer
's performBackgroundTask
had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With