Deleting an object in NSFetchedResultsController - ios

I need some advise on the following problem:
I am using iOS Master Detail application template for my app, i encounter problem when i segue from master to details page.
My question is why NSFetchedResultsChangeDelete is triggered when segueing ?
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
List *list = nil;
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;
}
}
But in my situation i want to do the following i.e. when the user deletes object by swiping table cell from right i also delete object from server, this works well when there is single table view and there is no segueing. If i prevent NSFetchResutlsController form deleting object when i segue to details like below then after coming back from details view the table view cell do not respond to my click events. i.e. the table does not segue, nothing happens on clicking,
Can any one guide what is going wrong here ? How should i achieve this functionality properly ?
case NSFetchedResultsChangeDelete:
list = (List*)anObject;
BOOL visible = [self.navigationController.topViewController isKindOfClass:[RKGMasterViewController class]];
if(list.listSyncStatus == [NSNumber numberWithInt:1] && visible)
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self deleteDataFromServer:list];
}

OK, first thing. The NSFetchedResultsController doesn't delete anything. It doesn't do any updates to the data store at all. All it does it fetch objects and watch for changes.
It sounds like you are confusing the tableview delete row with deleting something from the data store.
You shouldn't really be changing the NSFetchedResultControllerDelegate methods to do anything more than updating the table.
However, when the user swipes the cell and taps Delete you should then be deleting the relevant data from the core data store on a background thread. You should also (as part of this process) send a request to delete the data from the server too.
If you do the deletion on a background thread (and background context) then the NSFRC will pick up the change and remove the cell from the table for you.

Related

ios 7 unstable animation issue when deleting a row in a table view

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.

How do I make what my cell looks like update each time there is a change to the object in NSFetchedResultsController that corresponds to the cell?

I'm using Core Data in my app, and with it NSFetchedResultsController to use it in a table view. I can do various things to the cell/data, like swiping it left or right to delete or mark the cell as read respectively (this also deletes the data).
In cases where I say, mark the cell as read (which also sets the corresponding Core Data object's isRead attribute to YES), I want the cell to update visually to indicate this. The visual update is lightening the cell to look read (like what a read vs an unread email looks like).
How do I have this happen? (Update the cell when the object that corresponds to it has an attribute change, that is.)
I tried in this delegate method for NSFetchedResultsController:
- (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:(ArticleCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
And NSFetchedResultsChangeUpdate is the case that gets selected, so configureCell: is then called, which looks like this:
- (void)configureCell:(ArticleCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Article *article = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.article = article;
}
Which invokes my UITableViewCell subclass, ArticleCell's custom article property setter. In that method I basically start off with:
- (void)setArticle:(Article *)article {
if (_article != article) {
Comparing the new article to be set to the existing one for the cell (so it only updates if there's a change). However, the code inside this if statement is never called! It hops right past and exists the method. Within that if statement I have all the custom code to set what the cell looks like based on the properties of that article (isRead, kind, etc.)
How should I be handling this? I just want that whenever there is an update to the object in that corresponds to that cell, the cell reflects the update. Be it a change of read state, or whatever.
In the if statement you're comparing two references to the same object, so it will always be true when the setter is called from a change of type NSFetchedResultsChangeUpdate.
You don't really need the if, just trust the code in the controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: method and always update the cell if the method requests it.
It doesn't go inside that if because article object is the same. However its properties might be updated.
You might want to change the if or introduce some other method like -[ArticleCell update] that checks if any of the properties that affect the appearance of the cell are changed.

When I try to remove a UITableViewCell, I get this error: Invalid update: invalid number of rows in section 0, but I can't seem to fix it

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;
}
}

Dispatch a NSNotification based on a custom core data setter

I want to change a UITableViewCell based on whether or not a file exists in the documents directory. I feel like this should be Notification Based and the notifications should be sent when the objects isAvailable property has changed.
I don't want to create threading problems by accident. Since I am manipulating my core data objects on the main thread, should it be ok to setup a custom setter on my Concrete class to post a notification?
What would be the best way to do this? should I create my own notifications, or should I hook into something that core data already posts?
This is quite simple if you use NSFetchedResultsController. This class can be used in combination with a UITableView to reduce memory overhead and improve response time.
You can find docs at NSFetchedResultsController Class Reference and NSFetchedResultsControllerDelegate Protocol Reference.
In addition, you can implement delegate methods for NSFetchedResultsController. Implementing NSFetchedResultsController delegate methods, it allows you to listen for operations like add, remove, move, or update in your data (the NSManagedObjectContext you registered for) and, hence, in your table.
A very good tutorial on the subject is core-data-tutorial-how-to-use-nsfetchedresultscontroller. Here you can find all the elements to set up a UITableView, a NSFetchedResultsController and its delegate.
Said this, about your question you can use this technique to change the content of a UITableViewCell when isAvailable property (of a specific NSManagedObject) changes. In particular, you should implement the following delegate method to respond to specific changes (see the comment).
- (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: // <---- here you will change the content of the cell based on isAvailable property
[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;
}
}
Hope it helps.

"didChangeSection:" NSfetchedResultsController delegate method not being called

I have a standard split view controller, with a detail view and a table view. Pressing a button in the detail view can cause the an object to change its placement in the table view's ordering. This works fine, as long as the resulting ordering change doesn't result in a section being added or removed. I.e. an object can change it's ordering in a section or switch from one section to another. Those ordering changes work correctly without problems. But, if the object tries to move to a section that doesn't exist yet, or is the last object to leave a section (therefore requiring the section its leaving to be removed), then the application crashes.
NSFetchedResultsControllerDelegate has methods to handle sections being added and removed that should be called in those cases. But those delegate methods aren't being called for some reason.
The code in question, is boilerplate:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"willChangeContent");
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSLog(#"didChangeSection");
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 {
NSLog(#"didChangeObject");
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 {
NSLog(#"didChangeContent");
[self.tableView endUpdates];
[detailViewController.reminderView update];
}
Starting the application, and then causing the last object to leave a section results in the following output:
2011-01-08 23:40:18.910 Reminders[54647:207] willChangeContent
2011-01-08 23:40:18.912 Reminders[54647:207] didChangeObject
2011-01-08 23:40:18.914 Reminders[54647:207] didChangeContent
2011-01-08 23:40:18.915 Reminders[54647:207] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1145.66/UITableView.m:825
2011-01-08 23:40:18.917 Reminders[54647:207] Serious application error. Exception was caught during Core Data change processing: Invalid update: invalid number of sections. The number of sections contained in the table view after the update (5) must be equal to the number of sections contained in the table view before the update (6), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
As you can see, "willChangeContent", "didChangeObject" (moving the object in question), and "didChangeContent" were all called properly. Based on the Apple's NSFetchedResultsControllerDelegate documentation "didChangeSection" should have been called before "didChangeObject", which would have prevented the exception causing the crash.
So I guess the question is how do I assure that didChangeSection gets called?
Thanks in advance for any help!
This problem was caused by using a transient attribute as the sectionNameKeyPath. When I instead stored the attribute used for the sectionNameKeyPath in the database, the problem went away. I don't know if there is a way to get the sections updated based on a NSFetchedResultsController content changes when using a transient attribute as a sectionNameKeyPath. For now I am considering this a limitation of transient attributes.
I am doing the same thing in my application (transient property in the sectionNameKeyPath) and am not seeing the problem you are experiencing. I am testing this on iOS 4.2.1... There is a known bug where you cant trust any of the FRC delegate callbacks in iOS 3.X, you have to do a full [tableView reloadData] in the controllerDidChangeContent: message. see the FRC documentation
I have tested going from an existing section with another entry in it to a nonexistent section as well as from a section with only one row to another nonexistent section.

Resources