This is actually quite common due to an error in the Apple code label for NSFetchedResultsControllerDelegate that you get when creating a new wizard / Detailed project with the main data included:
- (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; } }
Solution # 1: use anObject
Why request a handler of selected results and risk using the wrong pointer path when the object is already provided to you? Martin R. recommends this solution .
Just replace the configureCell:atIndexPath: helper method with a pointer path to change the actual object:
- (void)configureCell:(UITableViewCell *)cell withObject:(NSManagedObject *)object { cell.textLabel.text = [[object valueForKey:@"timeStamp"] description]; }
In the cell for the row, use:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; [self configureCell:cell withObject:[self.fetchedResultsController objectAtIndexPath:indexPath]]; return cell; }
Finally, in the update, use:
case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; break;
Solution # 2: use newIndexPath
Starting with iOS 7.1, both indexPath and newIndexPath are passed when NSFetchedResultsChangeUpdate happens.
Just keep using the default implementation of indexPath when calling cellForRowAtIndexPath, but change the second index path that is sent to newIndexPath:
case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:newIndexPath]; break;
Solution # 3: Reload lines along a pointer path
Ole Begemann 's solution is to reload the pointer paths. Replace the call to configure the cell with the call to reload the lines:
case NSFetchedResultsChangeUpdate: [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break;
There are two drawbacks to this method:
- Causing a reload of the rows, it calls callForRow, which in turn calls dequeueReusableCellWithIdentifier, which will reuse the existing cell, it is possible to get rid of an important state (for example, if the cell is in the middle of a drag-and-drop Mailbox Style ).
- It is incorrect to try to reload a cell that is not displayed . In Apple source code, cellForRowAtIndexPath: will return "
nil if the cell is not visible or indexPath is out of range." Therefore, it would be more correct to check indexPathsForVisibleRows before causing a row reload.
Error playback
- Create a new master / detail project with master data in Xcode 6.4.
- Add title attribute to core data
event object. Fill the table with several entries (for example, in viewDidLoad run this code)
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext]; NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity]; for (NSInteger i = 0; i < 5; i++) { NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"]; [newManagedObject setValue:[@(i) stringValue] forKey:@"title"]; } [context save:nil];
Change the configuration cell to show the title attribute:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]]; }
In addition to adding an entry, when you press a new button, update the last item (Note: this can be done before or after creating the item, but be sure to do it before the call is called!)
// update the last item NSArray *objects = [self.fetchedResultsController fetchedObjects]; NSManagedObject *lastObject = [objects lastObject]; [lastObject setValue:@"updated" forKey:@"title"];
Launch the app. You should see five elements.
- Press the new button. You will see that a new element has been added to the top and that the last element does not contain the text โupdatedโ, although it should have used it. If you force the cell to reload (for example, scrolling the cell from the screen), it will have the text โupdatedโ.
- Now we implement one of the three solutions described above, and in addition to the added element, the last text of the element will be changed to "updated".