I'm getting no where with an issue that seems too stupid to exist. I have a UITableView that uses CoreData to generate objects.
I get an error returned here at [context save]:
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
Stop *stop=[self.fetchedResultsController objectAtIndexPath:indexPath];
[context deleteObject:stop];
NSError *error = nil;
if (![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
When I pass the stop object and a context (returned by a fresh call to [self.fetchedResultsController managedObjectContext]; another view and save it like so:
Stop *object= [[self fetchedResultsController] objectAtIndexPath:indexPath];
[[segue destinationViewController] setStop:object];
[[segue destinationViewController] setContext:[self.fetchedResultsController managedObjectContext]];
-(void)viewWillDisappear:(BOOL)animated
{
NSError *error = nil;
self.stop.locationText=self.textLocationField.text;
[self.stop.managedObjectContext save:&error];
if (![self.context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
The error is an NSMergeConflict which I understand occurs when multiple MOCs are acting on the same data. But I must be missing something fundamental because I don't believe I have two Contexts. Aren't I simply creating, modifying, and deleting the the object in a single context?
After a lot of trial and error turns out that you get an NSMergeConflict when you do not have by directional relationships. So if you have a person that has Feet your feet MUST point back to a person in the relationship graph.
Note that you do not need to pass the context around. You can access the property managedObjectContext that each NSManagedObject subclass provides. Thus
Stop *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSManagedObjectContext *context = object.managedObjectContext;
[context deleteObject:object];
// make sure you delete the table view cell as well
[context save:&error];
and for updating
self.stop.locationText = newText;
[self.stop.managedObjectContext save:&error];
The call to updatedObjects is not necessary. What did you want to accomplish with this?
Related
I have a method which allows the user to export a .csv file of all data in my Core Data model. I'm using the wonderful CHCSV Parser after performing a fetchRequest to fetch the stored results. Once the .csv has been created, it gets attached to an email and exported from the device. This all works fine, the issues come when I delete the data.
I am using a somewhat popular technique to delete my data (by popular I mean it has been recommended on lots of Stack Overflow answers I have came researched). I simply fetch all objects and delete them one by one.
// Create fetchRequest
NGLSAppDelegate *appDelegate = (NGLSAppDelegate *)[[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"NGLS"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
// Delete objects
for (Model *results in fetchedObjects) {
[context deleteObject:(id)results];
NSLog(#"NGLS objects deleted");
};
My model is simple and only has two entities, so I use the same code for the other Admin entity too. This is all fine, all objects are deleted, and I can confirm this by doing another fetchRequest which returns the object count for each entity - both have a count of zero. The problem occurs when I try to save data back to either entity after the delete has been executed.
The ViewController with the above code is an "Admin" control screen, where the user can login and also export the data. So when the user logs in, here is the code to save:
// Save login details
[_managedObjectAdmin setValue:user forKey:#"userLogin"];
[_managedObjectAdmin setValue:site forKey:#"siteLocation"];
NSError *error;
[[self.managedObjectAdmin managedObjectContext] save:&error];
I then perform the fetchRequest above and export all data when the "Export" button is pressed. After that is complete, when I try to login on the same ViewController, the output is:
2014-10-27 12:43:49.653 NGLS[19471:607] <NSManagedObject: 0x7a8c2460> (entity: Admin; id: 0x7a82e3e0 <x-coredata://3C9F3807-E314-439C-8B73-3D4459F85156/Admin/p30> ; data: <fault>)
If I navigate back to another ViewController (using my navigation controller), then go back to the "Admin" control screen and try to login again, I get this output:
2014-10-27 12:44:04.530 NGLS[19471:607] *** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x7a82e3e0 <x-coredata://3C9F3807-E314-439C-8B73-3D4459F85156/Admin/p30>''
(I can post the full log if requested).
I have tried many things in an attempt to resolve this issue. Core Data is complex and I dont quite understand what I need to do to fix this. I have tried to delete the persistentStore and create it again, and the same with the managedObjectContext but nothing I have tried has worked. I implemented a resetStore method in my AppDelegate to delete and rebuild the store as follows:
- (void)resetStores {
NSError *error;
NSURL *storeURL = [[_managedObjectContext persistentStoreCoordinator] URLForPersistentStore:[[[_managedObjectContext persistentStoreCoordinator] persistentStores] lastObject]];
// lock the current context
[_managedObjectContext lock];
[_managedObjectContext reset];//to drop pending changes
//delete the store from the current managedObjectContext
if ([[_managedObjectContext persistentStoreCoordinator] removePersistentStore:[[[_managedObjectContext persistentStoreCoordinator] persistentStores] lastObject] error:&error])
{
// remove the file containing the data
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//recreate the store like in the appDelegate method
[[_managedObjectContext persistentStoreCoordinator] addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];//recreates the persistent store
}
[_managedObjectContext unlock];
}
And also tried this method:
- (NSPersistentStoreCoordinator *)resetPersistentStore
{
NSError *error = nil;
if ([_persistentStoreCoordinator persistentStores] == nil)
return [self persistentStoreCoordinator];
_managedObjectContext = nil;
NSPersistentStore *store = [[_persistentStoreCoordinator persistentStores] lastObject];
if (![_persistentStoreCoordinator removePersistentStore:store error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
// Delete file
if ([[NSFileManager defaultManager] fileExistsAtPath:store.URL.path]) {
if (![[NSFileManager defaultManager] removeItemAtPath:store.URL.path error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
// Delete the reference to non-existing store
_persistentStoreCoordinator = nil;
NSPersistentStoreCoordinator *r = [self persistentStoreCoordinator];
return r;
}
But neither of these work. What I have found to work is after the export has completed, by closing and reopening the app everything works as normal again. I have tried to figure out what process occurs when closing/opening the app in regards to Core Data but I just don't know what to look for. I have researched many questions on Stack Overflow and tried all the solutions but nothing works for me. I really need some help on this, or else I'm going to have to force the user to quit the app after the export is done by a exit(0); command (because it's not getting submitted to the App Store, its for in-house employees) although I don't want that solution, surely there is a way I can just reset my database and continue to use the app without having to shut it down every time. Thanks in advance.
References:
Deleting all records from Core Data
Reset a Core Data persistent store
How do I delete all objects from my persistent store in Core Data
I have managed to solve the issue by simply refreshing the view. By calling my viewDidLoad method after the export is complete I create a new managedObject ready to use as normal, without leaving that ViewController or exiting the app altogether.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib
// Create fetchRequest
NGLSAppDelegate *appDelegate = (NGLSAppDelegate *)[[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Admin" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
// Fetch last object
Model *admin = [fetchedObjects lastObject];
// Populate login fields with last stored details
if (admin.userLogin == nil) {
//NSLog(#"Login empty");
} else {
self.usernameField.text = admin.userLogin;
self.siteLocationField.text = admin.siteLocation;
}
// Create new managed object using the Admin entity description
NSManagedObject *ManagedObjectAdmin;
ManagedObjectAdmin = [NSEntityDescription insertNewObjectForEntityForName:#"Admin"
inManagedObjectContext:context];
// Declare managed object
self.managedObjectAdmin = ManagedObjectAdmin;
// Save context
NSError *saveError = nil;
[context save:&saveError];
// ... more stuff
}
The solution was so simple all along. I will leave this question/answer up so other people can benefit from it if they find themselves in my situation. Thanks.
EDIT: Re-creating the managedObjectAdmin anywhere after the export works too, there is no specific need to call the viewDidLoad method. After the export has completed, I can simply create the new managedObject and save the context and continue using the app as normal, without having to navigate to another ViewController first or restarting the app.
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 use a singleton for working with arrays etc. cross the views in the application.
To initialize the singleton and the NSManagedObjectContext, so that I can fetch objects, I use:
+(DataControllerSingleton *)singleDataController
{
static DataControllerSingleton * single=nil;
#synchronized(self)
{
if(!single)
{
single = [[DataControllerSingleton alloc] init];
NSManagedObjectContext *context = [single.fetchedResultsController managedObjectContext];
single.masterCareList = [[NSMutableArray alloc] init];
}
}
return single;
}
When I insert a new object that object will not show up in display functions until I restart the application. I insert new object through this method in the singleton class:
- (void)insertNewObject:(Care *)care
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:
[entity name] inManagedObjectContext:self.managedObjectContext];
NSString *fileName = care.pictureURL;
NSString *text = care.causeText;
NSDate *date = care.date;
NSData *imgData = care.imageData;
[newManagedObject setValue:fileName forKey:#"urlPath"];
[newManagedObject setValue:text forKey:#"name"];
[newManagedObject setValue:date forKey:#"date"];
[newManagedObject setValue:imgData forKey:#"imageData"];
// Save the context.
[self saveContext];
NSError *error = nil;
if (![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should
not use this function in a shipping application, although it may be useful during
development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
My count method is the way I can tell that the new object is not included until I restart the application. The count method is also in the singleton as well.
- (NSUInteger)countOfList
{
NSArray *fetchedData = [_fetchedResultsController fetchedObjects];
return [fetchedData count];
}
When calling singleton I use:
DataControllerSingleton *singletonData = [DataControllerSingleton singleDataController];
[singletonData insertNewObject:care];
managedObjectContext property:
.h:
#property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
.m:
#implementation DataControllerSingleton
#synthesize managedObjectContext = _managedObjectContext;
#synthesize managedObjectModel = _managedObjectModel;
#synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
Why will not my new object show up in ex count until I restart application?
Am I somehow using different threads with different contexts, or different fethedResultsController or different singleton (shouldnt be possible right?)??
I added these two lines, which are not included in the genereated CoreData Stack, and it now works fine.
In singleton header:
#interface DataControllerSingleton : NSObject <NSFetchedResultsControllerDelegate>
In implementation file,
(NSFetchedResultsController *)fetchedResultsController {
_fetchedResultsController.delegate = self;
As I understand from your question, you are using a table or similar.
If you want to update the table as soon as you save the context you need to:
Reload the data table [table reloadData];
or implement in the correct delegate methods of (take a look to How To Use NSFetchedResultsController)
If you follow the first option, you can just do a save in the context and call realod data on the table.
NSError *error = nil;
if (![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should
//not use this function in a shipping application, although it may be useful during
development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[table reloadData];
NOTE THAT YOU ARE CALLING THE SAVE TWICE Do it once. In this case I suppose that [self saveContext]; does the saving as above.
If you follow the second approach, the data reload woul be handled for you.
Hope that helps.
Edit
The delegate of your fetched results controller should be a view controller (the one that contains the table). Do not put it in your singleton!!
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
[request2 setEntity:entity];
NSPredicate * predicate2 = [ NSPredicate predicateWithFormat:#"logoFrameNum == %#",[NSNumber numberWithInt:7]];
[request2 setPredicate:predicate2];
NSManagedObject * collectionList2 = [[ managedObjectContext executeFetchRequest:request2 error:&error2] objectAtIndex:0];
NSLog(#"context :%#", deleteContext1);
[managedObjectContext deleteObject:collectionList2];
BOOL yesorno = [collectionList2 isDeleted];
NSLog(#"yesorno : %i", yesorno);
NSError * error10;
NSLog(#"[managedObjectContext ] : %#", deleteContext1);
[collectionList2 release];
if (![managedObjectContext save:&error10]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error10, [error userInfo]);
exit(-1); // Fail
}
There is much more source above it. Change variables or get data from coredata is well performed with the same NSManagedObjectContex I have there. However delete with that context makes me crazy. It crashes without any error message just in
if (![managedObjectContext save:&error10]) {
I tried get a new context and so on and on........a lot..
You are performing a release on an object (collectionList2) that you don't own. This may cause a crash later on (for example, during the save). Try removing the release.
Maybe you are trying to delete a nil object.
Also, you should do all this within one single NSManagedObjectContext.
Try putting your save:error: method right below the deleteObject: call.