ManagedObjectContext cancel rollback not fully deleting inserted data - ios

I've seen a bunch of responses on similar threads but after following their recommendations am still having a bizarre issue and am banging my head against the wall.
I have a table of people and have the option for the user to add an entry for another user by filling out a form. That constructor is shown here:
-(id)initWithContext:(NSManagedObjectContext *)context {
self = [super initWithNibName:#"FamilyMemberInfoViewController" bundle:[NSBundle mainBundle]];
if (self) {
self.managedObjectContext = context;
mainUser = [NSEntityDescription insertNewObjectForEntityForName:#"FamilyMember" inManagedObjectContext:context];
Details *userDetails = [NSEntityDescription insertNewObjectForEntityForName:#"Details" inManagedObjectContext:context];
mainUser.details = userDetails;
userDetails.familyMember = mainUser;
...etc.
and then if the user clicks the back button I call:
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
...
[self.managedObjectContext rollback];
[mainUser release];
self.managedObjectContext = nil;
...
And when it goes to the parent table view controller, it reloads the data with the below code and, as expected, the object is gone.
-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == self) {
NSFetchRequest *familyMemberRequest = [NSFetchRequest new];
NSEntityDescription *familyDescription = [NSEntityDescription entityForName:#"FamilyMember" inManagedObjectContext:self.managedObjectContext];
familyMemberRequest.entity = familyDescription;
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"isMainUser == 0"];
[familyMemberRequest setPredicate:predicate];
NSError *error;
self.familyMembers = [managedObjectContext executeFetchRequest:familyMemberRequest error:&error];
NSLog(#"Retrieved family members! Count = %i", self.familyMembers.count);
[familyMemberRequest release];
}
}
HOWEVER - when I click away from the tab with that tableView and back to it, it runs the same code again and finds the cancelled object in the managed object context.
But - then when I re-run the app, it is nowhere to be found.
If I try creating another new person and backing out, it shows the correct number of entries initially (without any of the cancelled objects), and then when I click to another tab and back it shows only the most recent cancelled object in addition to the ones that should be there.
I've tried all manner of clearing the managed object context in the FamilyMemberInfoViewController: [context reset], [context rollback], [context deleteObject:], [context processPendingChanges] etc. I've tried experimentally loading the familyMembers array twice, and even when I click the back button it still finds the correct number of objects, but when I click a different tab and then back it shows the extra object (which then continues to show up if I go there and back).
Any ideas?

Related

CoreData Crashing Upon Deletion and More

I have some really mysterious behaviour with CoreData.
I'll add an Object. I save this object. I fetch the new results and reload the collection view (from which is display the objects). The new object shows up. Hoorah! Just as expected.
I do this a second time, but every time from now (unless the app is restarted) when re-fetching the data from my NSFetchedResultsController and reloading the collection view, the new object doesn't appear.
Equally, if I delete an object. First time, A-OK! The next time I do this, the app actually crashes with the following error:
(Aircraft is my NSManagedObject)
Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd0000000000c0000 <x-coredata://C418948D-90CD-40E9-A502-C4CAB0134419/Aircraft/p3>''
*** First throw call stack:
(0x18b79f09c 0x197ad5d78 0x18b4a77ac 0x18b4a6cac 0x18b4a6b00 0x100034438 0x18e6d8a44 0x18e6d6dc0 0x18e6d2e44 0x18e66ed78 0x18e26b0cc 0x18e265c94 0x18e265b4c 0x18e2653d4 0x18e265178 0x18e25ea30 0x18b75f7e0 0x18b75ca68 0x18b75cdf4 0x18b69db38 0x19106f830 0x18e6dc0e8 0x1000217dc 0x1980bfaa0)
libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException
Time for some code. I can't see any issues, but here it is. I won't spam you with everything, but if something rings any alarms, I can always add it on request.
Starting with the main view controller. This contains my collection view. Just as a note, it has two sections each fetching data from an individual NSFetchedResultsController. I am only seeing the issue with this specific one though. Fairly standard fetched results controller.
- (NSFetchedResultsController *)aircraftFetchedResultsController
{
if (_aircraftFetchedResultsController != nil) {
return _aircraftFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Aircraft" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:50];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.aircraftFetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.aircraftFetchedResultsController performFetch:&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();
}
return _aircraftFetchedResultsController;
}
Anywhere I use an NSManagedObjectContext I am getting it from my AppDelegate. When adding the new object, the user is in a modal (form sheet) view controller. I create a new object, but do not insert it immediately, incase the user cancels:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Aircraft" inManagedObjectContext:managedObjectContext];
self.aircraft = [[Aircraft alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
Then, when done, save the object:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
//Only need to insert the new object if its 'NEW' else just save the existing one we are editing
if (!isEditing)
{
//Create new aircraft
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
//We are definetly saving the object, so now we insert it
[managedObjectContext insertObject:self.aircraft];
}
//Save
[appDelegate saveContextWithCompletionBlock:^(BOOL didSaveSuccessfully) {
if (didSaveSuccessfully)
{
[self dismissViewControllerAnimated:YES completion:^{
[delegate addAircraftDidSave:YES];
}];
}
else
{
[self dismissViewControllerAnimated:YES completion:^{
//ALERT with error
}];
}
}];
I use a delegate to send a message back to the main view controller saying the object has saved. That method then fetches the new data and reloads the collection view to show the new object:
-(void)fetchAircraft
{
NSError *error;
if (![[self aircraftFetchedResultsController] performFetch:&error])
{
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
[UIAlertView showGenericErrorAlert];
}
//Success, we have results
else
{
[self.collectionView reloadData];
}
}
Done. As I said, this works first time, then start acting up. Equally, you can substitute the save code for the delete code I have, fairly similar, delete and save changes:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
[managedObjectContext deleteObject:self.aircraft];
[appDelegate saveContextWithCompletionBlock:^(BOOL didSaveSuccessfully) {
if (didSaveSuccessfully)
{
[self dismissViewControllerAnimated:YES completion:^{
[delegate addAircraftDidSave:YES];
}];
}
else
{
//ALERT with error
}
}];
(From my above comment:) The two fetched results controllers must use different
caches (cacheName: parameter). I also think (but I am not 100% sure about that)
that without sections, a cache does not give any advantages, so you can also
try cacheName:nil.
I believe you'll need to use separate ManagedObjectContexts for saves on a background thread.
https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html

Detecting # of Core Data objects in motionEnded:?

I'm trying to detect the amount of objects I have in Core Data when the user shakes the device. When I try and call a NSFetchRequest inside of motionEnded:, the simulator crashes with an unknown error at main.
Is doing a fetch like this inside of motionEnded: possible?
The code I have so far:
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
// see if we have albums to upload
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Groups" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSUInteger group_count = [managedObjectContext countForFetchRequest:fetchRequest error:nil];
if (group_count == 0)
{
// show alertview
}
else
{
// show another alertview
}
}
This is perfectly fine. You can trigger a search and an alert view based on the search result triggered by a gesture, including a shake gesture.
Check what exactly your error is, so pass an error object to your countForFetchRequest method. Possibly your managedObjectContext is nil or you have some problem with your entity name. ("Groups" would be a bad name for an entity. "Group" is much more logical.)

Core Data issue. Data won't save

So I have a utility app and I am trying to save some text into a "To" and "Message: text field on the Flipside View Controller. However, my data won't save. I am new to objective C and I have been using multiple different tutorials to the point where I have totally confused myself. Hopefully you can help me out. Not sure what else to do at this point...
FlipsideViewController.m
#import "CCCFlipsideViewController.h"
#import "CCCAppDelegate.h"
#import "CCCMainViewController.h"
#import "MessageDetails.h"
#interface CCCFlipsideViewController ()
{
// NSManagedObjectContext *context;
}
#end
#implementation CCCFlipsideViewController
#synthesize allMessageDetails;
#synthesize managedObjectContext;
- (void)awakeFromNib
{
[super awakeFromNib];
CCCAppDelegate *appDelegateController = [[CCCAppDelegate alloc]init];
self.managedObjectContext = appDelegateController.managedObjectContext;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
self.allMessageDetails = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
/*
NSManagedObject *managedObject; = [_fetchedResultsController valueForKey:#"to"];
self.toTextField.text = managedObject to;
messageDetails.to = [allMessageDetails firstObject];
self.toTextField.text = messageDetails.to;
messageDetails.message = [allMessageDetails valueForKey:#"message"];
self.messageTextField.text = messageDetails.message;
*/
NSLog(#"The 'to' is currently at %# after viewdidload", self.toTextField.text);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
return [textField resignFirstResponder]; //function says that if (bool) the text field is open and the keyboard hits return, text field is to resign first responder.
}
#pragma mark - Actions
- (IBAction)done:(id)sender
{
[self.delegate flipsideViewControllerDidFinish:self];
}
- (IBAction)resignFirstResponder:(id)sender {
[self.toTextField resignFirstResponder];
[self.messageTextField resignFirstResponder];
NSLog(#"Resigned First Responder");
}
- (IBAction)save:(id)sender {
// Create a new instance of the entity managed by the fetched results controller.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
// If appropriate, configure the new managed object.
[newManagedObject setValue:self.toTextField.text forKey:#"to"];
[newManagedObject setValue:self.messageTextField.text forKey:#"message"];
// Save the context.
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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
#pragma mark -
#pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"to" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![_fetchedResultsController performFetch:&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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
#end
I didn't look at all your code because there was a problem near the top that negates everything you do thereafter. Don't alloc/init your app delegate in awakeFromNib or anywhere else for that matter. The one and only instance of your app delegate already exists (I have no idea what happens when there is more than one app delegate).
CCCFlipsideViewController needs to gain access to the managed object context through another means. Perhaps CCCMainViewController (or another view controller) could set the CCCFlipsideViewController's managedObjectContext property. If CCCMainViewController does not have access to the managed object context, have the app delegate pass that context to it.
Example:
App delegate sets a managedObjectContext property on the root view controller; the root view controller, in turn, sets the managedObjectContext property on a child view controller (like your flipside VC), etc.
You don't seem to ever actually set self.messageTextField.text or self.toTextField.text to anything -- you have commented out code in your viewDidLoad method which sets these fields. bilobatum is entirely correct about your AppDelegate issue as well -- you could also use something like
[((NSObject*)[UIApplication sharedApplication].delegate) valueForKey: #"managedObjectContext"];
to get the app delegate for your application if you want to fix that fast, though long term bilobatum's solution to this is better design.
Honestly, I think you've done quite a number on this code... ;)
OK, first off, in your save method, don't create another NSManagedObjectContext, use the instance variable you already declared, "managedObjectContext."
Secondly, I think you've made things way too complicated for yourself... Storing Core Data is actually shockingly simple once you've created the NSManagedObject subclasses and set up everything in the App Delegate...
It seems as if you wouldn't need any info from the "fetchedResultsController" at that point in your code, since you're saving, not fetching. Maybe try changing your save method to something like:
- (IBAction)save:(id)sender {
NSEntityDescription *entity = [NSEntityDescription insertNewObjectForEntityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
// If appropriate, configure the new managed object.
[entity setValue:self.toTextField.text forKey:#"to"];
[entity setValue:self.messageTextField.text forKey:#"message"];
// Save the context.
NSError *error = nil;
[self.managedObjectContext save:&error]
if (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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
Edit: And to get the managed object context from the app delegate...
Right after your #synthesize's, create a variable for the App Delegate.
AppDelegate* appDelegateController;
And in viewDidLoad, initialize it:
appDelegateController = (AppDelegate*)[[UIApplication sharedApplication] delegate];
Right after viewDidLoad (or anywhere you want), you can stick in a method to declare the managed object context:
- (NSManagedObjectContext*)managedObjectContext {
return appDelegateController.managedObjectContext;
}
Then back in viewDidLoad, call that method with:
self.managedObjectContext = [self managedObjectContext];

Can not persist the deletion of a record using Core Data

I am using Core Data in a simple table view app which keeps track of a user's exercises. When one record is deleted, the deletion persists across that launch of the app. However, if I close the app and re-open it, the record re-appears. The user can delete the record from a detail view:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
if([title isEqualToString:#"OK"])
{
//delete record from database...
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"Parameters" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name like %#", parameters.name];
[request setPredicate:predicate];
NSError *error;
NSArray *matchingData = [context executeFetchRequest:request error:&error];
for (NSManagedObject *obj in matchingData) {
[context deleteObject:obj];
}
[self dismissViewControllerAnimated:YES completion:nil];
}
else if([title isEqualToString:#"Cancel"])
{
}
}
I can not figure out why the deleted record re-appears. This occurs on the simulator and on the device. I 'reset content and settings' on the simulator but still no luck.
Any suggestions are greatly appreciated. Thanks in advance.
Context is a "scratch pad". The "scratch pad" concept allows you to made changes locally in that "scratch pad", such as modifying the record or deleting the record, and discard the changes if you decide too.
You can also have multiple contexts or "scratch pads", usually one context in each thread, such as one for main thread, and another one for background thread.
Now, if you have decided that the changes are OK, you need to persist the changes by saving the context.
NSError *error = nil;
[context save:&error];
Of course, if you have other "scratch pads" in your app, you will need to sync those changes you have saved in the core data.
Use NSManagedObjectContext's save: method after you're finished doing changes to your Managed Object Models
Hope this helps!

efficiently display 100,000 items using Core Data

I am using a NSFetchResultsController to display 100,000 + records in a UITableView. This works but it is SLOW, especially on an iPad 1. It can take 7 seconds to load which is torture for my users.
I'd also like to be able to use sections but this adds at least another 3 seconds onto the laod time.
Here is my NSFetchResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (self.clientsController != nil) {
return self.clientsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
[request setPredicate:[NSPredicate predicateWithFormat:#"ManufacturerID==%#", self.manufacturerID]];
[request setFetchBatchSize:25];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"UDF1" ascending:YES];
NSSortDescriptor *sort2= [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObjects:sort, sort2,nil]];
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:#"Name", #"ManufacturerID",#"CustomerNumber",#"City", #"StateProvince",#"PostalCode",#"UDF1",#"UDF2", nil];
[request setPropertiesToFetch:propertiesToFetch];
self.clientsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
return self.clientsController;
}
I have an index on ManufacturerID which is used in my NSPredicate. This seems like a pretty basic NSFetchRequest - anything I can do to speed this up? Or have I just hit a limitation? I must be missing something.
First: you can use the NSFetchedResultsController's cache to speed up display after the first fetch. This should quickly go down to a fraction of a second.
Second: you can try to display the only the first screenful and then fetch the rest in the background. I do this in the following way:
When the view appears, check if you have the first page cache.
If not, I fetch the first page. You can accomplish this by setting the fetch request's fetchLimit.
In case you are using sections, do two quick fetches to determine the first section headers and records.
Populate a second fetched results controller with your long fetch in a background thread.
You can either create a child context and use performBlock: or
use dispatch_async().
Assign the second FRC to the table view and call reloadData.
This worked quite well in one of my recent projects with > 200K records.
I know the answer #Mundi provided is accepted, but I've tried implementing it and ran into problems. Specifically the objects created by the second FRC will be based on the other thread's ManagedObjectContext. Since these objects are not thread safe and belong to their own MOC on the other thread, the solution I found was to fault the objects as they are being loaded. So in cellForRowAtIndexPath I added this line:
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
object = (TapCellar *)[self.managedObjectContext existingObjectWithID:[object objectID] error:nil];
Then you have an object for the correct thread you are in. One further caveat is that the changes you make to the objects won't be reflected in the background MOC so you'll have to reconcile them. What I did was make the background MOC a private queue MOC and the foreground one is a child of it like this:
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_privateManagedObjectContext];
}
Now when I make changes in the main thread, I can reconcile them easily by doing this:
if ([self.managedObjectContext hasChanges]) {
[self.managedObjectContext performBlockAndWait:^{
NSError *error = nil;
ZAssert([self.managedObjectContext save:&error], #"Error saving MOC: %#\n%#",
[error localizedDescription], [error userInfo]);
}];
}
I wait for it's return since I'm going to reload the table data at this point, but you can choose not to wait if you'd like. The process is pretty quick even for 30K+ records since usually only one or two are changed.
Hope this helps those who are stuck with this!

Resources