NSRangeException exception in the NSFetchedResultsChangeUpdate NSFetchedResultsController event

I have a UITableView that uses NSFetchedResultsController as a data source.

The main data warehouse is updated in several background threads running in parallel (each stream uses its own NSManagedObjectContext ).

The main thread marks NSManagedObjectContextDidSaveNotification notify and update the context using mergeChangesFromContextDidSaveNotification:

It sometimes happens that an NSFetchedResultsController dispatches an NSFetchedResultsChangeUpdate event with an index that no longer exists at that moment.

For example: the result set of the recipient of the results contains 1 section with 4 objects. The first object is deleted in one thread. The last object is updated in another thread. Then sometimes the following happens:

  • controllerWillChangeContent: called.
  • controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: called with type = NSFetchedResultsChangeDelete, indexPath.row = 0.
  • controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: called with type = NSFetchedResultsChangeUpdate, indexPath.row = 3.

But the selected result controller contains only 3 objects, and if the call

 MyManagedObject *obj = [controller objectAtIndexPath:indexPath] 

To update the table view cell in accordance with the NSFetchedResultsChangeUpdate event, it fails with an NSRangeException .

Thanks for any help or ideas!

+8
core-data nsfetchedresultscontroller
source share
2 answers

Now I have found a solution for my problem. In the event of an update event, there is no need to call

 [self.controller objectAtIndexPath:indexPath] 

because the updated object has already been passed as anObject parameter to the -controller:didChangedObject:... delegate.

So I replaced -configureCell:atIndexPath: with the -configureCell:withObject: method, which directly uses the updated object. This seems to work without a problem.

Now the code is as follows:

 - (void)configureCell:(UITableViewCell *)cell withObject:(MyManagedObject *)myObj { cell.textLabel.text = myObj.name; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellIdentifier"]; [self configureCell:cell withObject:[self.controller objectAtIndexPath:indexPath]]; return cell; } - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { UITableView *tableView = self.tableView; switch(type) { /* ... */ case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; break; /* ... */ } } 
+13
source share

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".
+5
source share

All Articles