I have an app which shows a custom no-data-yet cell when the user first launches the app.
When the user makes the first entry the fetchedResults of my fetchedResultsController gets updated, which causes the no-data-yet cell to be deleted and a data cell to be inserted.
this used to work in the past (iPhone 3.x). Now on iOS 4.2, it results in a crash after endUpdates gets called. There is no exception information or any kind of intelligible stack trace. I only know the crash is caused in _CFTypeCollectionRetain (possibly trying to retain a NULL)
Any ideas how to proceed?
here is the relevant code:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"starting updates");
[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 *tv = self.tableView;
[tv reloadData];
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"insert");
if ([self.tableView numberOfRowsInSection:newIndexPath.section] == 1 &&
[[self.fetchedResultsController fetchedObjects] count] == 1)
{
NSLog(#"reloading row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else {
NSLog(#"inserting new row %d", newIndexPath.row);
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
NSLog(#"delete");
if ([self.tableView numberOfRowsInSection:newIndexPath.section] == 0 &&
[[self.fetchedResultsController fetchedObjects] count] == 0)
{
NSLog(#"reloading row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else {
NSLog(#"deleting row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"finishing updates");
[self.tableView endUpdates];
}
In the delegate method
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
you handle insertions and deletions. For insertions indexPath is nil. And for deletions newIndexPath is nil. So you are not allowed to access the newIndexPath in case of type== NSFetchedResultsChangeDelete.
Besides the call to reloadData is not necessary here.
Related
I creat a new project, select Master-Detail Application ,and select 'Use Core Data'
then build the new project. (XCode 5.0) this error:
ARC semantic Issue No visible #interface for 'UITableView' declares the selector 'cellForRowAtIndexPath:'
- (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:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
the error:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
That's because its a method on table view data source and not the table view. Also, the correct signature of the UITableViewDataSource method is tableView:cellForRowAtIndexPath: and not cellForRowAtIndexPath:
So your call should actually be,
[self configureCell:[datasource tableView:tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
// If datasource is self, replace with self
Hope that helps!
I have UITableView with NSFetchedResultsController attached to it. FRC's delegate methods work perfectly and everything is nice, but my cells have custom backgrounds that depend on the position of the cell:
1. on top of the UITableView (background with rounded corners on top)
2. in the middle (background with no rounded corners)
3. on the bottom (background with rounded corners on the bottom)
4. single cell (all corners are rounded).
In my cell configuration method I calculate a position of the cell and I'm setting an appropriate background view to it. Everything works fine, but there is a problem in the implementation of FRC's delegate method:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.agenciesTableView;
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:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
};
}
As you can see it is completely common, but there is one issue:
If the cell gets inserted to the top of UITableView, its configuration method (cellForRowAtIndexPath) gets called and it draws correctly (with rounded top corners) but the cell that is under it (that was previously on the top) remains with rounded top corners and I need to redraw it somehow.
-=EDITED=-
I changed FRC's delegate method to reflect the update logic:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we inserting a cell not to the middle of table view
//we need to update the previous cell (if we are inserting to the end)
// or the next cell (if we are inserting to the beggining)
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we deleting a cell not fromo the middle of table view
//we need to update the previous cell (if we are deleting from the end)
//or the next cell (if we are deleting from the beggining)
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row-1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
case NSFetchedResultsChangeUpdate:
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row-1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
};
}
But in this case I'm getting an error:
2013-05-24 21:04:19.458 PharmaTouch[6994:fb03] *** Assertion failure in -[_UITableViewUpdateSupport _computeRowUpdates], /SourceCache/UIKit_Sim/UIKit- 1912.3/UITableViewSupport.m:386
2013-05-24 21:04:19.483 PharmaTouch[6994:fb03] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid table view update. The application has requested an update to the table view that is inconsistent with the state provided by the data source. with userInfo (null)
-=EDITED2=-
here's my controller:cellExistsAtIndexPath: method implementation. It is used for safety checks to avoid reloadRowsAtIndexPaths calls with indexPath arguments that is out of bounds.
-(BOOL)controller:(NSFetchedResultsController *)controller cellExistsAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row<0)
{
return NO;
};
NSInteger numberOfRows = 0;
NSArray *sections = controller.sections;
if(sections.count > 0)
{
id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:indexPath.section];
numberOfRows = [sectionInfo numberOfObjects];
};
if (indexPath.row>(numberOfRows-1))
{
return NO;
}
else
{
return YES;
};
}
As you're using UITableViewRowAnimationNone you might as well just reloadData and then it will be done correctly because all rows will be refreshed.
Where you're using UITableViewRowAnimationFade, consider how much you value it...
You'd need to be a bit more comprehensive in your checks and reloading. In particular, you need to get the section info from the FRC for the section that's changing. If the incoming or out going row is the first or last, you need to refresh more than one row. Along the lines of:
if first is changing and count > 1, also refresh second
if last is changing and count > 1, also refresh first
I've solved the problem by adding a property #property (nonatomic, retain) NSMutableSet *indexPathsForRowsToReloadAfterFRCUpdates; to keep the rows that needs to be updated between controllerWillChangeContent: controllerDidChangeContent: method calls (in FRC delegate update cycle).
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
self.indexPathsForRowsToReloadAfterFRCUpdates=[[NSMutableSet alloc] init];
[self.agenciesTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeDelete:
{
[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.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we inserting a cell not to the middle of table view
//we need to update the previous cell (if we are inserting to the end)
// or the next cell (if we are inserting to the beggining)
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we deleting a cell not fromo the middle of table view
//we need to update the previous cell (if we are deleting from the end)
//or the next cell (if we are deleting from the beggining)
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
case NSFetchedResultsChangeUpdate:
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
};
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.agenciesTableView endUpdates];
if ([self.indexPathsForRowsToReloadAfterFRCUpdates count]>0)
{
[self.agenciesTableView reloadRowsAtIndexPaths:[[self.indexPathsForRowsToReloadAfterFRCUpdates allObjects] copy] withRowAnimation:UITableViewRowAnimationNone];
};
self.indexPathsForRowsToReloadAfterFRCUpdates=nil;
}
i was having problem while followed Brent Priddy's answer in this link
it looks everyone ok with the code, but i got exc_bad_access rite on the uisearchbar initialization code, here it is:
and there was warning: Unable to restore previously selected frame. in the console
anyone know what is wrong here?
update my post:
the actuall error when i put this code into my implementation file:
(void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView beginUpdates];
}
(void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)theIndexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self fetchedResultsController:controller configureCell:[tableView cellForRowAtIndexPath:theIndexPath] atIndexPath:theIndexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
(void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];
}
i hope someone can help me.
thanks
Try using only tableview, not self.tableview.
I have a simple UITableView Controller that shows CoreData. I'm trying to implement - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; and having trouble with the animation. The Core Data store gets updated, but the animation is not working.
How can I get the animation to correctly reflect the changes that are happening to the core data objects?
For example:
Initial order:
After item 2 to the top:
or, Initial Order:
After moving item 1 to position 3:
Here's the relevant code:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
//this implementation is from this tutorial: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [fetchedResultsController objectAtIndexPath:fromIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[toIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:#"order"];
}
NSLog(#"things: %#", things);
[things release], things = nil;
[managedObjectContext save:nil];
}
and the delegate:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"didChangeObject:");
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
The problem was caused by the delegate interfering with my reordering implementation.
I added a bool before saving my ManagedObjectContext:
reordering = YES;
[managedObjectContext save:nil];
and used it to skip out of any of the FetchedResultsController Delegate functions. Not the best solution, but it works for this implementation. I'd be grateful for any comments / answers explaining why this happened.
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (!reordering) {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
NSLog(#"controllerWillChangeContent:");
[self.tableView beginUpdates];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if (!reordering) {
NSLog(#"didChangeObject:");
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
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 {
if (!reordering) {
NSLog(#"didChangeSelection:");
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 {
if (!reordering) {
NSLog(#"didChangeContent:");
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}else {
reordering = NO;
}
}
I had the same problem, and I fixed it by moving the saving code to a "save" place. i.e.:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (!editing) {
//save here
}
}
I think it's understandable that things would be messed up if you save in the middle of moving things around. And -(void)setEditing seems to be a good place to do the saving.
btw, thanks for pointing out the cause of the problem!
The documentation says:
You should consider carefully whether you want to update the table view as each change is made. If a large number of modifications are made simultaneously—for example, if you are reading data from a background thread— /.../ you could just implement controllerDidChangeContent: (which is sent to the delegate when all pending changes have been processed) to reload the table view.
This is exactly what I'm doing: I'm processing incoming changes in a background thread with a different ManagedObjectContext, and merge the results into the main thread MOC with mergeChangesFromContextDidSaveNotification:. So far so good.
I chose to not implement controller:didChangeObject:... and would instead like to do the batched update that the document suggests.
Question/problem: the document doesn't elaborate how to actually implement the batched update? Should I just call [tableview reloadData] in controllerDidChangeContent: or is there a less intrusive way that saves me from a full reload?
One thought I have: I could take note of mergeChangesFrom... notification that contains the changed objects, figure out their indexpaths, and just call tableview:ReloadRowsAtIndexPaths: for them. But is there any authoritative info, recommendations or examples? Or just [tableview reloadData]?
(Aside: controller:didChangeObject:... started behaving really erratically when it received a set of batched updates, even though the same updating code [that I now put in background thread] was fine before when it was running on the main thread, but of course locking up the UI.)
I would just call reloadData in controllerDidChangeContent:.
For animating individual changes to the table, Apple's boiler plate code (iOS SDK 4.3.x) looks like this:
- (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];
}
I believe this is what you're looking for:
Instead of calling
deleteSections
insertSections
reloadSections
deleteRowsAtIndexPaths
insertRowsAtIndexPaths
reloadRowsAtIndexPaths
as the changes are coming in at controller:didChangeObject:atIndexPath:forChangeType:newIndexPath, collect the indexPaths and sections in properties. Then execute the batch in controllerDidChangeContent:.
Ref: http://www.fruitstandsoftware.com/blog/2013/02/19/uitableview-and-nsfetchedresultscontroller-updates-done-right/
Edit
Here is a slightly different, more contrived, but also more compact, version of the approach mentioned in the link:
#property (nonatomic, strong) NSMutableArray *sectionChanges;
#property (nonatomic, strong) NSMutableArray *objectChanges;
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
NSMutableDictionary *sectionChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
sectionChange[#(type)] = #(sectionIndex);
break;
}
[self.sectionChanges addObject:sectionChange];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
NSMutableDictionary *objectChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
objectChange[#(type)] = newIndexPath;
break;
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
objectChange[#(type)] = indexPath;
break;
case NSFetchedResultsChangeMove:
objectChange[#(type)] = #[indexPath, newIndexPath];
break;
}
[self.objectChanges addObject:objectChange];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = self.tableView;
NSUInteger totalChanges = self.sectionChanges.count + self.objectChanges.count;
if (totalChanges > 0)
{
[tableView beginUpdates];
if (self.sectionChanges.count > 0)
{
for (NSDictionary *change in self.sectionChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}];
}
}
else if (self.objectChanges > 0)
{
NSMutableArray *indexPathsForUpdatedObjects = [NSMutableArray new];
for (NSDictionary *change in self.objectChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[indexPathsForUpdatedObjects addObject:obj];
//[tableView reloadRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:obj[0] toIndexPath:obj[1]];
break;
}
}];
}
[tableView endUpdates];
for (NSIndexPath *indexPath in indexPathsForUpdatedObjects)
if ([tableView.indexPathsForVisibleRows containsObject:indexPath])
[self updateCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
[self.sectionChanges removeAllObjects];
[self.objectChanges removeAllObjects];
}
else if (totalChanges < 1)
{
[tableView reloadData];
}
}
- (NSMutableArray *)sectionChanges
{
if (!_sectionChanges)
_sectionChanges = [NSMutableArray new];
return _sectionChanges;
}
- (NSMutableArray *)objectChanges
{
if (!_objectChanges)
_objectChanges = [NSMutableArray new];
return _objectChanges;
}
Note that this solution is not perfect in that it does not update objects if there are changes to sections and those objects are in a section that is not updated. Should be easy to fix (and is irrelevant for many applications).
You could note the last index path in your datasource & then do this -
NSIndexPath *indexPath = nil;
NSMutableArray *newResults = [[NSMutableArray alloc] init];
if(gLastResultIndex < [self.dataSource count])
{
for(int i=(gLastResultIndex); i<[self.dataSource count]; i++)
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[newResults addObject:indexPath];
}
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:newResults withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates];
}
[newResults release];
This is selectively adding new rows to your existing UITableView. reload table reloads all cells in your table which you might not need. I use reload table only when the entire datasource changes...