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.
Related
A few users of my app are getting a crash with my TableView which uses a NSFetchedResultsController to get the data source from Core Data. My View controller is a subclass of CoreDataTableViewController which contains the standard code for a NSFetchedResultsController in a UITableViewController.
The crash report says that the crash happened in tableView:commitEditingStyle:forRowAtIndexPath:] at the line (code is just below) for this reason:
Fatal Exception: NSInvalidArgumentException
-deleteObject: requires a non-nil argument
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
}
I tried to look for a similar problem but I didn't find anything and it doesn't always happen (never experienced during testing).
Here is the didChangeObject in my CoreDataTableViewController class:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
{
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.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
Can anyone help me?
Here's something you won't see documented, but it's nevertheless sometimes true: This message can in rare circumstances be fired twice. If it is, the second time indexPath will be nil.
The appropriate way to handle this is to check for nil first:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
if (indexPath == nil) return;
[self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
}
I have had similar crashes in my delete routines for many months (possibly years?) and could never figure this out. Until one day, I got lucky and it happened while I was deleting rows with the debugger attached. I was able to see that indexPath was unexpectedly nil, and my array of items was already empty.
This means either the event was fired twice in a row, or the OS called it a second time in response to some action by the user after they had deleted the last item. I haven't been able to find out which, and I haven't seen it happen again. However, it explains a number of crash reports I've seen.
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.
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.
According to KVO observing of NSMangedObject in a UITableViewCell, one should implement NSFetchedResultsControllerDelegate rather than using KVO to observe propery changes on managed objects fetched with NSFetchedResultsController.
But how can I determined which properties have changed and on which objects?
I have data updates streaming in via a socket updating my managed objects, and want my UI to update accordingly whenever certain properties of listed objects change. I don't want to redraw the whole table, or each cell, only the label that displays the value of the property that changed, for example.
Is this doable, or should I resort to KVO but optimize it somehow?
You should not use KVO for updating CoreData objects. The reason being: the same object record can be retrieved from two separate contexts, and KVO observing would only notify you of the object from your view's context, leaving you unaware of updates made on the same object in a different context. Using the delegate methods from the fetched results controller allows you to respond to changes made on an object from other contexts. Here's example code taken from the 'CoreDataBooks' sample project, and is the proper way to respond to changes using a fetched results controller:
/*
NSFetchedResultsController delegate methods to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[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:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
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:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
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;
}
}
}