How can I update my fetchedResultsController upon tableView row selection? - iphone

Ok, I am getting better at this Core Data stuff, but I've got a ways to go. This is how I am populating my fetchedResultsController when my view loads:
- (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:#"Visit" 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:#"date" 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:#"Queue"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(isActive == YES)"];
[fetchedResultsController.fetchRequest setPredicate:predicate];
NSError *error = nil;
if (![self.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;
}
This works great. I have the fetchedResultsController hooked up to my tableView and I have 5 managed objects that are showing up. The problem I am running into is when I need to make a change to one of the managed objects.
As you can see in the predicate I am specifying that I only want managed objects that have isActive == YES. I need to change a managed object's isActive status to NO, and then remove it from the fetchedResultsController, and ultimately the tableView.
Here is how I am trying to do that:
-(void) seatedButton{
Visit * vis = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:reloadIndex inSection:0]];
vis.isActive = [NSNumber numberWithBool:NO];
NSError *error = nil;
if (![self.managedObjectContext 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();
}
[tableView reloadData];
}
It appears that this would work, but it doesn't. When the tableView is reloaded, 5 objects come back as fitting the requirements of the predicate, when it should only be 4!
What am I doing wrong here? What do I need to do to update the fetchedResultsController? Thanks!

There are delegate methods to the fetchedResultsController you may want to use that should do this all for you automatically. Once you save the context, it update the table for you, so you won't have to call [tableView reloadData]
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
EDIT: If you do this, you will need to implement the following method:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// configure cell;
}

I'm pretty sure adding the following code into your seatedButton method, just before the line with [tableView reloadData] will fix the problem, but not sure according to the doc that it should be necessary.
if (![self.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();
}

Related

Reloading UITableView without animation

What I'm trying to achive is UITableView reloading fast. Each cell has "checking" UIButton wchich tells user that item for this cell is selected. All selected cells should be on the end of the list (bottom cells).
I'm using NSFetchResultsController as a delegate and data source for UITableView. NSFetchResultsController is set to operate with "Item" entity with sectionKey selected for "checked" property (part od "Item" entity which )
- (id)initWithItemView:(ItemView*)iv{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:[CoreDataHandler context]];
[request setEntity:entity];
NSArray *predicates = [NSArray arrayWithObjects:[iv listDBUsingContext:[CoreDataHandler context]],nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"lista IN %# AND deletedDB == NO", predicates];
[request setPredicate:predicate];
[request setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"checked" ascending:YES selector:#selector(compare:)];
NSSortDescriptor *sortDescriptor2
= [[NSSortDescriptor alloc] initWithKey:#"position" ascending:YES selector:#selector(compare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, sortDescriptor2, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
[sortDescriptor2 release];
if (self=[[FastTableDelegate alloc]
initWithFetchRequest:request
managedObjectContext:[CoreDataHandler context]
sectionNameKeyPath:#"checked"
cacheName:nil])
{
self.delegate = self;
self.itemView = iv;
self.heightsCache = [NSMutableDictionary dictionary];
}
[request release];
[self performFetch:nil];
return self;
}
Then when the user tap UIButton in cell I change the "checked" property in NSmanagedObject (Item entity) and save the context. Then my NSFetchReslutsController is notified that one item changed it state and it performs UITableView reload using functions
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.itemView.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
}
Because I changed "checked" property which is sectionKey for my NSFetchedResultsController UITableView should be reloaded with this scenario
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
And here is a problem because when cells are inserted or delated UITableView performs animation which duration is about 0.5s. In this time UITableView stop reciving events for next cell so when the user "check" another cell it will not be notified about that tap. What I'm trying to achive is to enable user to quick checking some cells without waiting for animation to end.
Possible solutions:
Use reloadData istead of deleteRowsAtIndexPaths/insertRowsAtIndexPaths. It will be no animation but reloading all UITableView last longer that reloading one cell so in my case user will wait for UITableView to reload - again waiting. IS there a way to avoid animation without reloading all UITableView?
Enable user to check cells, but save context only after 0.5s after last cell selection. User can select fast multiple cell and when he ends selecting all changes are propagated to UITableView - my boss want to send each selected cell to second section without grouping then, one reload for each cell selected - again bad idea
Maybe some custom tableView (user created, not apple) ?
Primary goal:
Enable user to "check" cells quickly refreshing UITableView after each "checking"
I'm open for any ideals :)
Reload your table in -(void)viewDidAppear:(BOOL)animated
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[yourTable reloadData];
}

Core Data Insertion Error

I have a to-many relationship with Meals and Food (i.e. Meal <<------>> Food). Food is an abstracted class, and can be either a Fruit or Vegetable. I have a RootViewController that displays all the Food in a given Meal. In one class I add a Vegetable and another I add a Fruit.
I get the following error when I start adding these to a Meal. I am really not sure what is going on and how this is happening.
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
2012-03-19 22:52:06.652 iGlucoTouch[682:11903] Meal Name: Meal #1 atIndex: 0
RootViewController.h
#interface RootViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate>
{
NSFetchedResultsController *_fetchedResultsController;
NSManagedObjectContext *_context;
UITableView *_tableView;
}
#property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
#property (nonatomic, retain) NSManagedObjectContext *context;
#property (nonatomic, retain) UITableView *tableView;
#end
RootViewController.m
#implementation RootViewController
#synthesize fetchedResultsController = _fetchedResultsController;
#synthesize context = _context;
#synthesize tableView = _tableView;
- (void)viewDidAppear:(BOOL)animated
{
self.tableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 0, 320, 400) style:UITableViewStyleGrouped];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.rowHeight = 60.0;
[self.view addSubview:self.tableView];
[self.tableView release];
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
self.context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
Food *food = [_fetchedResultsController objectAtIndexPath:indexPath];
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.meal removeFoodsObject:food];
NSError *error;
if (![self.context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
}
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
self.context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
Food *food = [_fetchedResultsController objectAtIndexPath:indexPath];
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.meal removeFoodsObject:food];
NSError *error;
if (![self.context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
}
}
- (NSFetchedResultsController *)fetchedResultsController
{
self.context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Food" inManagedObjectContext:self.context];
[fetchRequest setEntity:entity];
NSPredicate *foodPredicate = [NSPredicate predicateWithFormat:#"ANY meals == %#", self.meal];
[fetchRequest setPredicate:foodPredicate];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.context sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[sort release];
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
I add a Fruit or Vegetable like this in another class:
self.context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
Fruit *fruit = [_fetchedResultsController objectAtIndexPath:indexPath];
[self.meal addFoodsObject:fruit];
NSError *error;
if (![self.context save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
Apparently I was not setting the fetchedResultsController and the context to nil. I was in the dealloc, but that was not getting called because this was one of the views in a tab bar which was declared at the application delegate. So I simply added the code below. Maybe it is because I am sharing a context.
- (void)viewDidDisappear:(BOOL)animated
{
self.fetchedResultsController = nil;
self.context = nil;
}
Yes - sounds like you didn't add the new entry to the NSArray that the NSFetchResultsController is backed by. Right after you do [self.meal addFoodsObject:fruit] you should also be able to call something like
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath]
withRowAnimation: UITableViewRowAnimationRight];
yourself. Jeff LaMarche's link is a great explanation.

Getting An Assertion Failure Error

I am getting the following error when loading one of my views that has a UITableView in it. Does anyone know how to fix it? I have already tried deleting the (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath method, but that didn't help.
I'm thinking it has to do with the table's delegate not being updated properly with numberOfRowsInSection or something.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-1448.89/UITableView.m:995
2011-06-05 00:38:12.116 App[14523:707] Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 3 deleted). with userInfo (null)
Here is my code:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete the managed object for the given index path
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
NSLog(#"fetched results : \n%#\n",[self.fetchedResultsController fetchedObjects]);
// Commit the change.
NSError *error = nil;
// Update the array and table view.
if (![managedObjectContext save:&error])
{
// Handle the error.
}
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}
#pragma mark - Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil)
{
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Set" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat: #"sets == %#", self.exercise]];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:YES];
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:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![self.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();
}
NSLog(#"fetched results : \n%#\n",[self.fetchedResultsController fetchedObjects]);
NSLog(#"fetch count: %i", [fetchedResultsController.fetchedObjects count]);
return self.fetchedResultsController;
}
#pragma mark - Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.setsTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.setsTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.setsTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.setsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.setsTableView endUpdates];
[self.setsTableView reloadData];
}
is Try uncommenting [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
Please note:
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
should return a value 1 less than that of before calling 'deleteRows..'
Basic problem is this: You are instructing UI to delete one of the rows, but not removing that row from the back end.
If you are using the code from CoreDataBooks sample you might want to change this:
- (void)addControllerContextDidSave:(NSNotification*)saveNotification {
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
}
with
- (void)addControllerContextDidSave:(NSNotification*)saveNotification {
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
});
}
This will prevent NSFetchedResultsController from refreshing while the tableView is not visible because of the UINavigationController transition.
Of course this thing occurs only in simulator, because on the device the insert will take longer than the [self dismissModalViewControllerAnimated:YES] animation.
In my case when I called [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES]; only, I got this type of error. I solved this by removing the object from Array (from this array I am displaying data.) and called [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
I put each section elements in separated arrays. Then put them into another array( arrayWithArray). My solution here for this problem:
[quarantineMessages removeObject : message];
[_tableView beginUpdates];
if([[arrayWithArray objectAtIndex: indPath.section] count] > 1)
{
[_tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indPath] withRowAnimation:UITableViewRowAnimationBottom];
}
else
{
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:indPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
[_tableView endUpdates];

Problem with NSFetchedResultsController and inserting records

Ok, this one is driving me crazy and I've tried everything I can find on the internet as a possible solution but still nothing. So here is my situation:
I have a view that has a button on it. When this is touched it pops up a list of customers for the user to select from. when they select it, i make a use the fetchedresultscontroller to get the parts associated to the customer and display them in a tableview. This all works well. the problem is if I have Customer A selected and insert a new part, then go to Customer B and insert a new part, when i reselect Customer A and try to insert another part the app crashes with the following error:
* Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1447.6.4/UITableView.m:976
2011-02-21 10:39:12.896
SalesPro[36203:207] Serious
application error. An exception was
caught from the delegate of
NSFetchedResultsController during a
call to -controllerDidChangeContent:.
Invalid update: invalid number of rows
in section 0. The number of rows
contained in an existing section after
the update (1) must be equal to the
number of rows contained in that
section before the update (1), plus or
minus the number of rows inserted or
deleted from that section (1 inserted,
0 deleted). with userInfo (null)
2011-02-21 10:39:12.907
SalesPro[36203:207] * Terminating
app due to uncaught exception
'NSRangeException', reason:
'-[UITableView
scrollToRowAtIndexPath:atScrollPosition:animated:]:
row (1) beyond bounds (1) for section
(0).'
Code that handles selecting of Customer
-(void) CustomerSelectedRaised:(NSNotification *)notif
{
NSLog(#"Received Notification - Customer Selected");
selectedCustomer = (Customer *)[notif object];
[self buildCustomerInfoText];
if (popoverController != nil) {
[popoverController dismissPopoverAnimated:YES];
}
fetchedResultsController = nil;
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle error appropriately
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); //fail
}
[self.partsListGrid reloadData];
}
FetchedResultsController code
#pragma mark -
#pragma mark Fetch results controller
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
//set-up fetched results controller
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PartsList" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSLog(#"TAMS ID: %#", selectedCustomer.customerTAMSID);
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"customerTAMSID == %#", selectedCustomer.customerTAMSID]];
//set to sort by customer name
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"sortOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:[self managedObjectContext]
sectionNameKeyPath:nil cacheName:nil];
[aFetchedResultsController setDelegate:self];
[self setFetchedResultsController:aFetchedResultsController];
//clean-up
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
//return results
return fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
{
[[self partsListGrid] beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// In the simplest, most efficient, case, reload the table view.
[[self partsListGrid] endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.partsListGrid;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath withHeight:tableView.rowHeight];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.partsListGrid insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.partsListGrid deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
default:
break;
}
}
Add Part code
-(void) addScannedPart:(Part *)part
{
// Check to see if entered part is already in list
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *partsEntity = [NSEntityDescription entityForName:#"PartsList" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:partsEntity];
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"customerTAMSID == %# AND lineAbbreviation == %# AND partNumber == %#", selectedCustomer.customerTAMSID, part.lineAbbrev, part.partNumber];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchedParts = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if ([fetchedParts count] == 0) {
//Create a new instance of the entity managed object by the fetched results controller
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
NSLog(#"Entity Name: %#", [entity name]);
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
//Add fields to Managed Object
int sortOrder = [[fetchedResultsController fetchedObjects] count];
sortOrder++;
[newManagedObject setValue:[part lineAbbrev] forKey:#"lineAbbreviation"];
[newManagedObject setValue:[part partNumber] forKey:#"partNumber"];
[newManagedObject setValue:[NSNumber numberWithInt:[[part orderQty] intValue]] forKey:#"orderQuantity"];
[newManagedObject setValue:selectedCustomer.customerTAMSID forKey:#"customerTAMSID"];
[newManagedObject setValue:[NSNumber numberWithInt:sortOrder] forKey:#"sortOrder"];
//Save the context
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
//reload Customer list
NSIndexPath *insertionPath = [fetchedResultsController indexPathForObject:newManagedObject];
[self.partsListGrid selectRowAtIndexPath:insertionPath animated:YES scrollPosition:UITableViewScrollPositionTop];
[self.partsListGrid reloadData];
}
}
This is the biggest (most severe) defect I have to figure out for our next release (which is very soon). I'd appreciate any and all help! Thanks!
start with fixing the leaking NSFetchedResultsCOntroller in
-(void) CustomerSelectedRaised:(NSNotification *)notif
{
/*...*/
fetchedResultsController = nil;
/*...*/
}
you shouldn't have multiple NSFetchedResultsControllers that all report changes to the same tableview. And I think this is what is happening. Unless the NSFRController gets deallocated it will report changes to its delegate
ok, thanks to this SO thread, it looks like I have it working:
I edited by CustomerSelectedRaised method to look like so:
-(void) CustomerSelectedRaised:(NSNotification *)notif
{
NSLog(#"Received Notification - Customer Selected");
selectedCustomer = (Customer *)[notif object];
[self buildCustomerInfoText];
if (popoverController != nil) {
[popoverController dismissPopoverAnimated:YES];
}
//this is the new code
NSFetchRequest *fetchRequest = [[self fetchedResultsController] fetchRequest];
NSEntityDescription *entity = nil;
entity = [NSEntityDescription entityForName:#"PartsList" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"customerTAMSID == %#", selectedCustomer.customerTAMSID]];
[NSFetchedResultsController deleteCacheWithName:nil];
// end of new code
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle error appropriately
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); //fail
}
[self.partsListGrid reloadData];
}

Is there a problem with my Core Data code

I have had a customer contact me about a bug in my application which I believe is related to my use of Core Data. This is currently only a theory as I have not be able to re-create the issue myself.
The problem appears to be if the user creates a new entity object and then edits the entity object straight away it does not seem to persist correctly and the next time the user opens the application the object is not there.
If the user creates the entity object and exits the app without editing it the problem does not occur, and if the user edits the object after re-opening the app it also does not occur.
I have only had one report of this happening and the device in question is a iPhone 3G running IOS4.0.2. I can not re-create it on an iPhone 3GS, iPhone 4 or via the simulator (all running 4.0.2)
I am no Core Data expert and am wondering if there is a problem with the way I am using the framework. I would really appreciate it if someone would review the code I have below and let me know if they see any potential problems.
I have included the methods that interact with core data from the relevant classes.
AppDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSError *error = nil;
if (managedObjectContext_ != nil) {
if ([managedObjectContext_ hasChanges] && ![managedObjectContext_ save:&error]) {
// log
}
}
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSError *error = nil;
if (managedObjectContext_ != nil) {
if ([managedObjectContext_ hasChanges] && ![managedObjectContext_ save:&error]) {
// log
}
}
}
- (NSManagedObjectContext *)managedObjectContext {
if (managedObjectContext_ != nil) {
return managedObjectContext_;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext_ = [[NSManagedObjectContext alloc] init];
[managedObjectContext_ setPersistentStoreCoordinator:coordinator];
}
return managedObjectContext_;
}
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel_ != nil) {
return managedObjectModel_;
}
managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
return managedObjectModel_;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator_ != nil) {
return persistentStoreCoordinator_;
}
NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"mydatabase.sqlite"]];
NSError *error = nil;
persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"An error occurred adding the persistentStoreCoordinator. %#, %#", error, [error userInfo]);
// Display an alert message if an error occurs
}
return persistentStoreCoordinator_;
}
Controller 1 (creates the entity object using an image selected via the Image Picker)
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
UIImage* selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
// Create an image object for the new image.
NSManagedObject *image = [NSEntityDescription insertNewObjectForEntityForName:#"Image" inManagedObjectContext:managedObjectContext];
// Set the image for the image managed object.
[image setValue:[selectedImage scaleAndCropImageToScreenSize] forKey:#"image"];
// Create a thumbnail version of the image.
UIImage *imageThumbnail = [selectedImage thumbnailImage:50.0F];
// Create a new note
Note *note = [NSEntityDescription insertNewObjectForEntityForName:#"Note" inManagedObjectContext:managedObjectContext];
NSDate *now = [[NSDate alloc] init];
note.createdDate = now;
note.lastModifiedDate = now;
[now release];
note.thumbnail = imageThumbnail;
note.image = image;
}
[self dismissModalViewControllerAnimated:YES];
[picker release];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NotePadViewController *notePadController = [[NotePadViewController alloc] initWithNibName:#"NotePadView" bundle:nil];
Note *note = [[self fetchedResultsController] objectAtIndexPath:indexPath];
notePadController.note = note;
notePadController.title = note.notes;
[self.navigationController pushViewController:notePadController animated:YES];
[notePadController release];
}
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Note" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"createdDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"An error occured retrieving fetched results. %#, %#", error, [error userInfo]);
// Display an alert message if an error occurs
}
return fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self setControllerTitle];
[self.tableView endUpdates];
}
Controller 2 - Used to view and edit the entity object (add text)
- (void)saveAction:(id)sender {
NSDate *now = [[NSDate alloc] init];
if (self.note != nil && textView.text.length == 0) {
self.note.notes = nil;
self.note.lastModifiedDate = now;
} else if (textView.text.length != 0) {
// Create a new note if one does not exist
if (self.note == nil) {
VisualNotesAppDelegate *appDelegate = (VisualNotesAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
Note *newNote = [NSEntityDescription insertNewObjectForEntityForName:#"Note" inManagedObjectContext:managedObjectContext];
self.note = newNote;
self.note.createdDate = now;
}
self.note.lastModifiedDate = now;
self.note.notes = textView.text;
self.title = [note title];
}
[now release];
[self.textView resignFirstResponder];
}
Many thanks in advance.
First, you really should not be ignoring those errors. You could be throwing an error there and never know it.
Nothing really jumps out about the code itself, but you could easily be having an error during the save, perhaps a non-optional parameter not being set, etc.
Handle the exceptions and see from there.
The problem is that I am only saving the managed object context when the app terminates or is backgrounded.
I only have about 5 seconds for all of the image blobs that were created by the user to be saved before the app exits. If there are a lot of images then they may not all save in time which is why they do not appear when the app is restarted.
This mainly effects the iPhone 3G as it is the slowest of all the models I support.
I have updated my code to save the managed context when ever a change is made. This has resolved this problem but has exposed another issue:
https://stackoverflow.com/questions/3578951/nsfetchedresultscontrollerdelegate-methods-not-being-called-when-camera-image-sav
If the entity is only created in the saveAction: method then it might not get created in the first place if the user quits the application before manually choosing save.