I have a simple table view with 1 section and 2 rows. I'm using a NSFetchedResultsController to keep the table sync'd with CoreData. I make a change to one of the rows in CD which triggers a table view cell to be updated and moved. The problem is that when cellForRowAtIndexPath: gets called during the NSFetchedResultsChangeUpdate, the wrong cell is returned (this makes sense b/c the cells haven't been moved yet). So the wrong cell is updated with with the newly updated data. After that the NSFetchedResultsChangeMove message is handled so the cells trade places (neither cell's content is updated since its just a move call). The result is both cells reflect the data from the newly updated CD entity. Reloading the table fixes the issue. I'm running iOS 6.
In other words if the cell at index 0 represents entity A and index 1 represents entity B and I update entity A to A' in such a way that the 2 cells reverse order, the result is that I see 0:A' 1:A when I would expect 0:B, 1:A'.
- (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:
//the wrong cell is updated here
[self configureCell:(SyncCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
//this code produces errors too
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
//[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
A solution is:
[self configureCell:(SyncCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:newIndexPath ? newIndexPath : indexPath];
Using the new index path when its supplied during the update. And then use delete and insert instead of move. I'd still like to know if anyone else has any input.
I would recommend you to take a look at this blogpost
Fixing the bug is easy. Just rely on the behavior of UITableView I described above and replace the call to configureCell:atIndexPath: with the reloadRowsAtIndexPaths:withRowAnimation: method, which will automatically do the right thing:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
When invoking configureCell, look up the indexPath based on the object passed in. Both indexPath and newIndexPath are unreliable at this point. For example:
case NSFetchedResultsChangeUpdate:
myPath = [controller indexPathForObject:anObject];
if (myPath) {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:myPath];
}
break;
The only solution that worked for me: Thomas Worrall's Blog: Reordering rows in a UITableView with Core Data
First of all, Its necessary create an attribute in your NSManagedObject to hang the last order of your object.
#interface MyEntity : NSManagedObject
#property (nonatomic, retain) NSNumber * lastOrder;
#end
Then, declare a property in your ViewController.m:
#interface ViewController ()
#property (nonatomic) BOOL isReordering;
#end
In method tableView:moveRowAtIndexPath:toIndexPath:, manage the changes in a temporary array:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
_isReordering = YES;
NSMutableArray *arrayNewOrder = [[_fetchedResultsController fetchedObjects] mutableCopy];
MyEntity *myManagedObject = [arrayNewOrder objectAtIndex:fromIndexPath.row];
[arrayNewOrder removeObjectAtIndex:fromIndexPath.row];
[arrayNewOrder insertObject:myManagedObject atIndex:toIndexPath.row];
for (int i=0; i < [arrayNewOrder count]; i++)
{
myManagedObject = [arrayNewOrder objectAtIndex:i];
myManagedObject.lastOrder = [NSNumber numberWithInt:i];
}
_isReordering = NO;
NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}
success = [[self managedObjectContext] save:&error];
if (!success)
{
// Handle error
}
}
Thomas explained about perform fetch and save context:
I'm not entirely sure why the fetch needs to be performed first, but
it fixed a ton of crazy bugs when I did it!
The last trick is handling NSFetchedResultsController delegate method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, specifically in NSFetchedResultsChangeMove:
case NSFetchedResultsChangeMove:
{
if (!_isReordering)
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
It worked for me! I hope It helps you!
Related
My app currently uses a regular UITableView, which is populated via a NSFetchedResultsController. I also use the NSFetchedResultsControllerDelegate to update the table view. The rows are grouped by sections, which represent dates. This is also handled by the NSFetchedResultsController.
It works quite well, except when heavy changes to the sections are done in one step. I give an example here:
My tableview looks like this:
[10/10/2016]
ROW 0.0
[10/08/2016]
ROW 1.0
ROW 1.1
Now I want to change the item ROW 0.0. I change the date from 10/10/2016 to 10/09/2016.
// EDIT //
To be precise: My NSManagedObject subclass has a property NSDate* startingDate;, which is stored in the database. And when the user hits a save button I get an NSDate object from my UIDatePicker, make sure it's not nil and set the property to it. Afterwards I save the context.
// EDIT //
What happens is that the NSFetchedResultsControllerDelegate get's called three times. First with a delete section 0 and then with and insert section 0 (although those two changed order when I executed several times). And then with an update row 0.0.
And what happens is: Nothing. The Tableview is not updated at all (section headers stay the same) and the row which should have been changed is in some strange state where it can be selected (the delegate method for row selection is called) but a different row is highlighted. And when I scroll down no new rows are loaded (I can scroll down, but it only shows empty space).
My NSFetchedResultsControllerDelegate is implemented as follows:
- (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;
case NSFetchedResultsChangeMove:
case NSFetchedResultsChangeUpdate:
break;
}
}
- (void) controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[[self tableView] reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[[self tableView] deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[[self tableView] insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
}
I have tried another implementation which collects all change requests, optimizes them and executes all of them within - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller, but it had the same result.
Any help on this would be greatly appreciated.
Thank you very much!
In my app, I manage a library of music files using CoreData as the backing store. Works pretty well. I also use an NSFetchedResultsController to bind the data to a table view for display, which is also all fine and dandy. The issue arises when I try to enable editing on the table view.
As usual, I return YES from tableView:canEditRowAtIndexPath: to allow rows to be editied, enabling the swipe to delete UI. That works nicely and shows up right. However, after deleting files and whatnot, the UITableView begins having some strange rendering issues:
I don't send any messages to the tableview to explicitly request UI updates, since the fetched results controller notifies my UITableViewController subclass of this, which then sends the appropriate messages:
/**
* Handler for changing an entire section at once
*/
- (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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationLeft];
break;
}
}
/**
* Handler for changing a single object, usually a row.
*/
- (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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
/**
* Called when the fetched results controller begins sending update messages.
*/
- (void) controllerWillChangeContent:(NSFetchedResultsController *) controller {
[self.tableView beginUpdates];
}
/**
* Called when the fetched results controller finishes sending updates.
*/
- (void) controllerDidChangeContent:(NSFetchedResultsController *) controller {
[self.tableView endUpdates];
}
After reading the docs, I'm pretty sure all I need to do is delete the object from the context, which happens like this:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// perform on the object context's background queue
[[SQUPersistence sharedInstance].managedObjectContext performBlock:^{
[[SQUPersistence sharedInstance].managedObjectContext deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database
[[SQUPersistence sharedInstance] save:nil];
}];
}
}
Eventually, the UI enters a strange state as seen above and the table gets out of sync with the backing store, and no animations are performed. Am I doing this entire deletion thing wrong, or have I encountered a bug with Apple's classes?
I suspect it is threading issue. If you're deleting on the background queue, but referencing the object from the main queue, [_fetchedResultsController objectAtIndexPath:indexPath], I am sure that will cause problems.
Right off, I would confirm by trying:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = _fetchedResultsController.managedObjectContext;
[context deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database (You do not have to do this here)
[context save:nil];
}
}
If you do not have issues doing it this way, I'd suggest leaving it. There is likely little benefit in deleting a single object in the background. Where you might hang would be the save operation, which you don't actually need to do here. You can leave the save till user exits or some other point at which any lag would be masked by the UI change.
I have a table view with a search bar/display controller (all built in story board).
My problem is with deleting rows. The functionality of delete row works BUT the animation of row deletion seems to be unstable. Sometimes the deletion is animated and most of the times the row is removed immediately with no animation.
This is my delete method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
if (tableView == self.searchDisplayController.searchResultsTableView) {
[_managedObjectContext deleteObject:[_notesFilteredFetcher objectAtIndexPath:indexPath]];
} else {
[_managedObjectContext deleteObject:[_notesFetcher objectAtIndexPath:indexPath]];
}
NSError *error;
if (![_managedObjectContext save:&error]) {
DLog(#"Failed deleting note : %#", [error localizedDescription]);
}
}
}
The table view is using core data so the deletion of the rows occur in the appropriate controller's method as follows:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = (controller == _notesFilteredFetcher) ? self.searchDisplayController.searchResultsTableView : 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 forTable:tableView];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I also noticed that when animation for delete does occur, all rows above the deleted row move a bit down and the rows under the deleted row move up until the empty space is covered.
If I look at the built-in mail app and delete some rows (i.e. mails) it does seem to animate as expected every time and ONLY the rows below the deleted row are moved up to cover the empty space (the rows above the deleted row do not move).
Any ideas ?
UPDATE:
I have noticed that animation does occur only for the last row. If I delete any other row no animation occurs but if I delete the last row animation does work.
If someone bumps into the same problem :
I noticed that I called reloadData method of the table view right after the call to endUpdates of the same table view as I needed to update the cell's internal indices.
This will cause the animation to stop immediately. Removing the reloadData method solved the animation issue.
I have a gesture on a UITableViewCell subclass called ArticleCell, so when it is swiped a method in the UITableViewController class gets called to delete the cell that was swiped.
The delegate method looks like this:
- (void)swipedToRemoveCell:(ArticleCell *)articleCell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:articleCell];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
[self.tableView reloadData];
}
But every time I swipe, I get this error:
Invalid update: invalid number of rows in section 0
More information: It uses Core Data for the data source, so it uses NSFetchedResultsController. Do I have to update something there? (I haven't touched any of its methods.)
You always need to also remove the row from your data source object as well. You will need to remove it from your Core Data store at the same time as you delete the row representing the data from the table view itself.
The issue is this mismatch, you remove the row from the table view but your -numberOfRowsInTableView data source method is still returning the old number of rows because the fetched results controller still sees that number in the data store.
it is happening because when you are deleting the row but you are not deleting the actual object from the list, therefore it is returning wrong number of counts for rows or either for section. you should update your list as well.
One more thing you don't need to reload your data as it is doing already when you deleting the row.
Use this directly by removing that object from array and reloading tableView after that:
- (void)swipedToRemoveCell:(ArticleCell *)articleCell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:articleCell];
[DATASOURCE_ARRAY removeObjectAtIndex:indexPath.row]; // Here DATASOURCE_ARRAY is the array you are using as datasource of tableView
[self.tableView reloadData];
}
Hope it helps you.
Since you are using core data , you can do this
In your delete action call this code
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.fetchedResultsController.managedObjectContext deleteObject:object];
Overwrite this fetchcontroller delegate ....
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableViewIB;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationMiddle];
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;
}
}
Even though I have worked with NSFetchedResultsController dozens of times, I have run into a problem I can't seem to find the cause of.
I have a UIViewController with a NSFetchedResultsController that manages the data source of a table view. Whenever I delete an object in the NSFetchedResultsController, the table view is updates by the fetched results controller, but when I save the NSManagedObjectContext (to commit the delete), an insert is triggered for the object that was just deleted. The net result is that the object is not removed from the table view.
The odd thing is that when the application is restarted, the object that was supposed to be deleted is actually deleted from the store and doesn't show up in the table view.
To be clear, I use only one managed object context, which is only accessed on the main thread. In other words, the setup is quite simple.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Fetch Store
CCDStore *store = [self.fetchedResultsController objectAtIndexPath:indexPath];
// Delete Store
[self.fetchedResultsController.managedObjectContext deleteObject:store];
// Save Changes
NSError *error = nil;
[self.fetchedResultsController.managedObjectContext save:&error];
if (error) {
NSLog(#"Error %# with user info %#.", error, error.userInfo);
}
}
}
Even though the deleted object is inserted in the fetched results controller, the object is turned into a fault, but I don't understand why it is added to the fetched results controller in the first place.
No exceptions or errors are thrown. I have performed several checks such as calling isDeleted and validateForDelete on the object to find the cause, but I have not become any wiser.
EDIT:
I am pretty sure that the fetched results controller's delegate methods are not the culprit, but for completeness I have added the implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
[self updateView];
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:(CCDStoreCell *)[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;
}
default: {
break;
}
}
}