I have an NSManagedObjectContext
associated to main thread (mainContext
), where I fetch all the NSManagedObject
I show throughout the app.
Users don't edit these objects, but I get updates from web services. I periodically perform asynchronous calls to such services, and they "tell" me which of my managed objects have to be deleted (if any), which of them have to be updated with new information (if any), and if I need to insert new objects.
So, I need to firstly get the responses of all the services and then check what changes I have to make to the managed objects I already have in my mainContext
. And I also need to perform the updates avoiding blocking the UI.
I was thinking about 2 ways to manage this scenario:
- To use a completely separated
privateContext
in a private queue with its own Core Data stack to insert there all the objects I get from services. Then compare somehow (how?) with the objects I have in mainContext
, and delete/modify/insert objects that are there in mainContext
.
- To use a
privateContext
in a private queue, but being a child of the mainContext
. I'd then need to pass the child context the objects I have in its parent mainContext
(is this possible? how?), and at the same time insert in this child context the objects I get from services, and then compare and perform changes.
What of the approaches would be the best or the appropriate one? Or maybe should it be a different one I haven't thought about?
Thanks in advance
EDIT: Could this be another possible way?:
- Only use the
mainContext
and, as I'm parsing the responses of the services, instead of creating the new objects just make changes on mainContext
one by one...
EDIT 2: Another possibility?:
- Only use the
privateContext
, get the services responses and create the new objects. Then, also fetch with this privateContext
all the objects that already existed (and that would be the same as the objects in mainContext
). Make changes in this privateContext
comparing the two sets of objects (the recently created from services and the fetched), save this context, clear mainContext
and re-fetch all objects in mainContext
.
I'm not sure this is a full answer for you but I'm working on a somewhat similar situation. The implementation path I've taken is to use child objects (all over the place) - or more like temporary child context's as needed.
The first thing I should mention, however, is make sure to use the CoreData debug functionality build into XCOde. I made a second Run-Scheme that has
-com.apple.CoreData.ConcurrencyDebug 1
On application init I have my normal NSManagedObjectContext
- which is passed around to my background networking thread.
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[child setParentContext:parentObjectContext];
Then whenever I need to pass something from parent to child I end up doing:
[child objectWithID:(object-in-parent-context)]
or I end up doing
__block AHRS_RPYL * ret;
[[self getChildContext] performBlockAndWait:^{
ret = [NSEntityDescription insertNewObjectForEntityForName:@"RPYL" inManagedObjectContext:[self getChildContext]];
// ... lots of code
}];
I can't say I really "love" this approach and I'm sort of stuck with it for the moment however it does seem to work well enough.
Between most of my view controllers I have a
@synthesize managedObjectContext;
And in my prepareForSegue
method
// Pass on managedObjectContext
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// dispatch_async(dispatch_get_main_queue(), ^{
// If the destination VC is able to take teh setManagedObjectContext method the current objectContext will be passed along.
if ([segue.destinationViewController respondsToSelector:@selector(setManagedObjectContext:)]) {
[segue.destinationViewController performSelector:@selector(setManagedObjectContext:)
withObject:self.managedObjectContext];
} else {
NSLog(@"Segue to controller [%@] that does not support passing managedObjectContext", [segue destinationViewController]);
}
// });
}
Summary
- I've worked with your second choice option and it is doable although it feels kind of clunky in my honest opinion. The truth is we are looking into switching to Realm instead of
CoreData
because no matter how you look at it CoreData
is not the most threading friendly option.
UDPATES
Save Code
I actually save to the child context every 50 messages and to the parent every 250 it looks like (haven't touched this code in ages). I can't promise this is the best correct way to do things but it does work and it does keep disc access to a minimum. I am getting A LOT of messages like 20+ a second so i wanted to do this stack type. You may not care
(self.getChild()) returns the child context - its older code i left here.
if (msgCount % 50 == 0) {
// Child Save!
// NSLog(@"Saving SDatas");
__block NSManagedObjectContext *currentChild = [self getChildContext];
[self incChildContext];
// Parent-Child save methodology
[currentChild performBlock:^{
NSError *error;
if (![currentChild save:&error]) {
NSLog(@"\n error => %@ \n", [error localizedDescription]);
NSLog(@" error => %@ ", [error userInfo]);
[NSException raise:@"Database Write Error" format:@"%@ %@", [error localizedDescription], [error userInfo]];
// abort();
}
if (msgCount % 250 < 5) {
[parentObjectContext performBlock:^{
NSError *error;
if (![parentObjectContext save:&error]) {
NSLog(@"\n error => %@ \n", [error localizedDescription]);
NSLog(@" error => %@ ", [error userInfo]);
[NSException raise:@"Database Write Error" format:@"%@ %@", [error localizedDescription], [error userInfo]];
// abort();
}
}];
}
[currentChild reset];
}];
}
Deleting
[childContext performBlock:^{
// LOTS OF CODE
// to build a query set of the records we want to kill
// End lots of code
[childContext deleteObject:msg];
if (i % modFactor == 0) {
self.percentDone = i / totalRecords;
NSLog(@"%.1f Saving ...", self.percentDone * 100);
NSError *error;
if (![childContext save:&error]) {
NSLog(@"\n error => %@ \n", [error localizedDescription]);
NSLog(@" error => %@ ", [error userInfo]);
[NSException raise:@"Database Write Error" format:@"%@ %@", [error localizedDescription], [error userInfo]];
// abort();
}
[parentContext performBlock:^{
NSError *errrror;
if (![parentContext save:&errrror]) {
NSLog(@"\n error => %@ \n", [error localizedDescription]);
NSLog(@" error => %@ ", [error userInfo]);
[NSException raise:@"Database Write Error" format:@"%@ %@", [error localizedDescription], [error userInfo]];
// abort();
}
}];
}
}];
...
Again - like i said before this may not be the best way of doing things but i do have a parent/child stack and it does work. This was one of the first iOS apps I wrote and to do it over again i'd punt on core data and use something else.
We had an extremely high update rate so when we were using a single stack it was making the UI very slow. The migration to parent/child was slow - were i to do it again I think it would have gone smoother because i'd write many helper functions to deal with some of this (or just use swift).
good luck.