NOTE: I've looked at similar questons, but didn't find one that describes this situation.
I'm looking at the following example code from Apple regarding Core Data concurrency (https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/Concurrency.html)
NSArray *jsonArray = …;
NSPersistentContainer *container = self.persistentContainer;
[container performBackgroundTask:^(NSManagedObjectContext *context) {
for (NSDictionary *jsonObject in jsonArray) {
AAAEmployeeMO *mo = [[AAAEmployeeMO alloc] initWithContext:context];
[mo populateFromJSON:jsonObject];
}
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Failure to save context: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
}];
In my app, the save is not initiated until the user taps the save button on the screen. How do I go about it, should I use a child context instead for that situation, where the private context is a property of the VC?
NSArray *jsonArray = …; //JSON data to be imported into Core Data
NSManagedObjectContext *moc = self.persistentContainer.viewContext; //Our primary context on the main queue
NSManagedObjectContext *private = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[private setParentContext:moc];
[private performBlock:^{
for (NSDictionary *jsonObject in jsonArray) {
NSManagedObject *mo = …; // WHICH CONTEXT TO USE? <<<======
//update MO with data from the dictionary
}
NSError *error = nil;
if (![private save:&error]) {
NSLog(#"Error saving context: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
}
And then once the user taps save do this:
NSManagedObjectContext *moc = self.persistentContainer.viewContext; //Our primary context on the main queue
[moc performBlockAndWait:^{
NSError *error = nil;
if (![moc save:&error]) {
NSLog(#"Error saving context: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
}];
}];
Also note the question which moc to use in the example above (<<<=====)
EDIT: What I ended up doing in the end is save the child context immediately so that the table only uses viewContext to display the results. If the user then exits without saving, I delete all the results again from the viewContext. The save button is still there, but now only sets a flag indicating not to delete the results.
If you have one page of forms and you want it to save when the user presses the save button, then simply take the data from the textFields (or whatever your data input is) and put it into core data using performBackgroundTask. Since the data is only stored in the textFields while the user is editing if the user pushes back his edits will be lost.
If you have a lots of changes to a complex document with lots of different entities that the user can create or destroy or link and all of that is only saved when the user presses save then you should use a child context. You would display the data based on the values in the child context but only push those changes to the parent context if the user presses save. This is a very rare situation and I have never personally encountered the need to do this.
I strongly suspect that you are in the first case. Don't use child context. Use performBackgroundTask and save the data when the user presses save.
(also the correct context to use inside the block [private performBlock:^{ is the private context)
Related
I'm using three-leveled multithreaded Core Data introduced by Marcus Zarra in his book. tempMOC for dealing with difficult tasks, mainMOC for UI management and writerMOC for writing data to persistant store.
I'm trying to integrate this model with my UITableView.
Everytime user pull-to-refresh I process downloading, parsing and loading this data. In this process there is one extra step - deleting previos entries for entit. I want this to be smooth so, the current UITableView (MOC as well) will wait to be cleaned until the last moment so this gap between deleting and loading new data wouldn't last for c.a 4 sec.
Here is my method which is called everytime I pull-to-refresh (I've removed parsing to keep the code cleaner):
- (void)loadTimetableToCoreData:(id)timetable
{
[self.pullToRefresh finishLoading];
// Initializing temporary context
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.parentContext = self.moc;
[tempContext performBlock:^{
// Parsing JSON data
}];
[self deleteAllObjects:#"Timetable"];
NSLog(#"Finished loading to temp MOC");
[tempContext performBlock:^{
// Saving procedure with multithreading
NSError *error;
if (![tempContext save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to temp MOC");
[self.moc performBlock:^{
// Save groups to presistant store
NSError *error;
if (![self.moc save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to main MOC");
[self.writer performBlock:^{
// Save groups to presistant store
NSError *error;
if (![self.writer save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to writer MOC");
}];
}];
}];
}
I've also put some logs and if you run this code it's like:
2014-04-08 09:55:28.349 devPlan[21125:1803] Finished loading to temp MOC
2014-04-08 09:55:33.145 devPlan[21125:1803] Finished saving to temp MOC
2014-04-08 09:55:33.650 devPlan[21125:60b] Finished saving to main MOC
2014-04-08 09:55:33.652 devPlan[21125:60b] Finished saving to writer MOC
So as you can see there is this gap between loading and saving to temp MOC. It's okey because there is a lot of work going on but I would like to wait with [self deleteAllObjects:#"Timetable"]; until this work is done. When this is executed it wipes all data and when it's realoaded again it's displayed in UITableView - but still with time gap in which UITableView is empty...
What should I do to resolve thing thing? Below is the list of what I've tried so far:
Putting this delete method in various places across download method.
Messing around with MOCs performBlock: and performBlockAndWait:.
Fetching and initializing deleteMOC with NSFetchRequest, waiting for data being processed and later call delete method.
I must tell you, I'm stuck with this one... And it bothers me so much but I think there must be a logical explanation to this!
Edit
Here is the code of the method responsible for deleting:
- (void)deleteAllObjects:(NSString *)entityDescription
{
// Initializing temporary context
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.parentContext = self.moc;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityDescription inManagedObjectContext:tempContext];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *items = [tempContext executeFetchRequest:fetchRequest error:&error];
[tempContext performBlockAndWait:^{
for (NSManagedObject *managedObject in items) {
[tempContext deleteObject:managedObject];
}
NSError *error;
if (![tempContext save:&error]) {
NSLog(#"Error deleting %# - error:%#", entityDescription, [error localizedDescription]);
}
}];
}
First the easy answer; I would put the -deleteAllObjects after the save of the tempContext.
But I would question the idea of deleting everything from the table. Could you not just remove the objects that need to be removed, add the objects that need to be added and update the ones that need to be updated? By capturing the notification from NSManagedObjectContextDidSaveNotification you can resolve that data and present (imho) the data more cleanly.
Update
I am not concerned with the parsing (insert, update, delete) part of it as who cares how long it takes. It is human perceivable no matter what so we are not going to do magic there. I am thinking more of the UX of the table view disappearing and then reappearing as opposed to individual cells updating and resorting themselves. I personally feel that is a far cleaner, slicker looking experience.
The doubling up sounds like there is something wonky in your NSFetchedResultsController delegate methods or in your -deleteAllObjects. Care to post that?
Update
Ok so you are literally deleting everything from that table. Is there a reason you are deleting vs. merging?
Now that I understand a bit more, I would do this order of events:
Process the data
Block the UITableView from updating with a -beginUpdates
Delete all data
Save both contexts
Unblock the UITableView with a -endUpdates
That should give you a more "instantaneous" refresh of the tableview.
I am not a fan of the delete/insert way of handling data but it may be appropriate in your case.
With a little help of my older code I've came onto this:
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:#"Timetable"];
[r setIncludesPendingChanges:NO];
NSArray *existingTimetables = [tempContext executeFetchRequest:r error:nil];
for (Timetable *table in existingTimetables) {
[tempContext deleteObject:table];
}
So I'm fetching Timetable and not messing it with pending changes inside tempContext. With this code everything works like it should but is blocking main thread! But I guess this qualifies to be another question.
I have a Core Data stack with a main managed object context with NSMainQueueConcurrencyType.
The user can initiate a task on a managed object that can take a long time, so it is performed on a separate context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
Person *samePerson = (Person *)[context objectWithID:person.objectID];
[context performBlock:^{
// BLOCK 1
// do lots of work
// then update the managed object
samePerson.value = someCalculatedValue;
// save the private context
NSError *error;
if (![context save:&error]) {
NSLog(#"Error: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}];
This works fine, and the main MOC gets updated properly, NSFetchedResultsController hooked up to it perform properly, etc.
The problem is with deleting. I have this setup to delete objects:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
[context performBlock:^{
// BLOCK 2
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSError *error;
NSArray *all = [context executeFetchRequest:request error:&error];
if (!all) {
NSLog(#"Error fetching: %#", error);
} else {
for (NSManagedObject *person in all) {
[context deleteObject:person];
}
NSError *error;
if (![context save:&error]) {
NSLog(#"Error saving: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}
}];
Now if I do this delete operation (Block 2) during the time it takes to perform the long-duration task (Block 1), then the delete operation finishes quickly, and saves to main context. After a while Block 1 finishes, and when it saves the mainMOC at its end, we get a seemingly obvious crash:
CoreData could not fulfill a fault for ...
My question is: how do I perform a task such as Block 1 with the possibility of its object being deleted?
If these are both background tasks that cannot run at the same time, try using semaphores to protect access to them.
eg. for an instance variable:
dispatch_semaphore_t _backgroundProcessingSemaphore;
Lazily initialised using something like:
- (dispatch_semaphore_t)backgroundProcessingSemaphore
{
if (!_backgroundProcessingSemaphore) {
_backgroundProcessingSemaphore = dispatch_semaphore_create(1);
}
return _backgroundProcessingSemaphore;
}
Surround the critical code with:
dispatch_semaphore_wait(self.backgroundProcessingSemaphore, DISPATCH_TIME_FOREVER);
// Critical code
dispatch_semaphore_signal(self.backgroundProcessingSemaphore);
Only one critical section of code can then run at any point in time. The block that calls dispatch_semaphore_wait will block if the semaphore is already taken, until it is freed up.
You also probably want to think about splitting your long-duration task up so that it will run in discrete batches if you're not already doing so - this is useful if the long running background task timer is about to expire while you still have work to do - you can stop and restart from the appropriate point on next launch.
Other options would involve forcing a save on block 1 before block 2 saves itself, but this starts to get messy. Much easier to ensure the two competing blocks cannot overlap.
At the moment I can't see any AUser objects in my sqlite3 app database.
This is the code I have to create a user. Am I missing something? There are no warnings/errors with the code.
AUser *user = [NSEntityDescription insertNewObjectForEntityForName:#"AUser" inManagedObjectContext:_managedObjectContext]; // _managedObjectContext is declared elsewhere
user.name = username; //username is some string declared elsewhere / name is an attribute of AUser
You need to perform a save on the context.
NSError* error = nil;
if(![context save:&error]) {
// something went wrong
NSLog(#"Error saving context: %#\n%#", [error localizedDescription], [error userInfo]);
abort();
}
When you perform a save, data in the context are saved to the persistent store coordinator (hence in your sql store).
P.S. Based on the discussion with #G. Shearer, in production if the context fails to save, you can handle the error gracefully. This means not using abort() call that causes the app to crash.
NSError* error = nil;
if(![context save:&error]) {
// save failed, perform an action like alerting the user, etc.
} else {
// save success
}
You need to call save after creating the object.
Example:
NSError *error;
if ([user.managedObjectContext save:&error]) {
//Success!
} else {
//Failure. Check error.
}
you need to call
NSError *error = nil;
[_managedObjectContext save:&error];
if(error != nil) NSLog(#"core data error: %#",[[error userInfo] description]);
before the object will be persisted to the database
My app simply add some users informations (name, birthdate, thumbnail, ...) with Core Data.
I noticed that if I delete a user right after created it, my app just stop working (not a crash, xCode returns no crash log, nothing).
I'm using asynchronous nested context for saving my users informations so I guess that behavior is due to the fact that my delete statement is executing before my save statement.
But since i'm a total beginner with Core Data, i don't really know how to handle that. I don't even know if i declared nested contexts the right way.
Here's my save codes :
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.backgroundManagedObjectContext;
BSStudent *newStudent = (BSStudent *)[NSEntityDescription insertNewObjectForEntityForName:kBSStudent inManagedObjectContext:tmpContext];
newStudent.firstname = firstname;
newStudent.lastname = lastname;
newStudent.birthdate = birthdate;
newStudent.thumbnail = thumbnail;
newStudent.createdAt = [NSDate date];
[self dismissViewControllerAnimated:YES completion:nil];
[tmpContext performBlock:^{
[tmpContext save:nil];
[self.backgroundManagedObjectContext performBlock:^{
NSError *error;
if (![self.backgroundManagedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
[self.managedObjectContext performBlock:^{
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
}];
}];
For precision, self.managedObjectContext is a NSPrivateQueueConcurrencyType and self.backgroundManagedObjectContext is a NSMainQueueConcurrencyType. And self.backgroundManagedObject is a child of self.managedObjectContext.
Here's my delete codes :
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
Can someone know how to handle this situation properly ?
Your delete is probably using the BSStudent created by a different context than you are deleting with. The following code will fix that.
NSManagedObjectContext * deleteContext = student.managedObjectContext;
[deleteContext deleteObject:student];
If you really want to use the other context, refetch the student using ObjectID
NSManagedObject * studentToDelete = [self.managedObjectContext objectWithID:student.objectID];
[self.managedObjectContext deleteObject:studentToDelete];
Nested contexts tips
Your contexts are probably okay, but I see a lot of people throwing around performBlock unnecessarily. With nested contexts, the QueueConcurrencyType refers to the thread it will do Core Data operations on, not the thread it was created on. So doing an operation like save on itself inside its performBlock is unnecessary and can lead to deadlocks.
When you save a child context, the parent is automatically synced with the changes. If you want to save upwards to the next higher parent automatically, I would recommend registering the parent for NSManagedObjectContextDidSaveNotification of the child saves. You can make this easier by having your AppDelegate have a factory method for creating the child contexts.
- (NSManagedObjectContext *)createChildContext
{
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.managedObjectContext;
//Register for NSManagedObjectContextDidSaveNotification
return tmpContext;
}
if you wrap your delete in a performBlock call it can't execute at the same time as the saving performBlock.
e.g.:
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext performBlock:^{
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
This is the "preferred" way of dealing with contexts as it serializes access to the context and keeps all those operations on the contexts thread,
I assume you are getting the crash because the objectID is becoming invalid or changing before the save completes, near the top of the call stack you'll see something about "hash64" or such
I'm using core data to save some integer (rate) and then I call save in the context:
HeartRateBeat * beat = [HeartRateBeat heartRateWithHeartRate:rate
ofRecordTitle:self.recordTitle
inManagedObjectContext:document.managedObjectContext];
NSError * error;
[document.managedObjectContext save:&error];
Inside that convenient method I create the object using NSEntityDescription like this:
heartRateBeat = [NSEntityDescription insertNewObjectForEntityForName:#"HeartRateBeat" inManagedObjectContext:context];
(I only copied some important code, just to show what I did.)
I immediately execute a fetch request after every single heart beat inserted and managed object context saved (I save immediately), and the request shows that heart beat does appear to be stored inside Core Data (with growing array of heart beats), but if I restart my app (I'm using simulator BTW) I know things aren't actually getting saved to disk because it starts anew. Checking with SQLite3 command line shows empty tables. What am I missing here?
I get the same problem but I think its just because, I assume, like me you are just stopping the app through xcode and not actually closing it down. I use this code to force a write. Im using a UIManagedDocument, shared through appdelegate, rather than setting everything up manually.
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[[AppDelegate sharedAppDelegate].userDatabase saveToURL:[AppDelegate sharedAppDelegate].userDatabase.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
I don't know about you guys, but when I want my Core Data to save, my Core Data better save.
Here's some code that will for sure save all of your Core Datas.
-(void)forceSave {
NSManagedObjectContext * context = self.managedObjectContext; //Get your context here.
if (!context) {
NSLog(#"NO CONTEXT!");
return;
}
NSError * error;
BOOL success = [context save:&error];
if (error || !success) {
NSLog(#"success: %# - error: %#", success ? #"true" : #"false", error);
}
[context performSelectorOnMainThread:#selector(save:) withObject:nil waitUntilDone:YES];
[context performSelector:#selector(save:) withObject:nil afterDelay:1.0];
[context setStalenessInterval:6.0];
while (context) {
[context performBlock:^(){
NSError * error;
bool success = [context save:&error];
if (error || !success)
NSLog(#"success: %# - error: %#", success ? #"true" : #"false", error);
}];
context = context.parentContext;
}
NSLog(#"successful save!");
}
Note that this is BAD CODE. Among other problems, it's not thread-safe and takes too long. However, try using this and deleting some parts of it to your satisfaction :)