NSFetchedResultsController objectAtIndex, objectAtIndexPath, indexPathForObject inconsistencies - objective-c

I have a fetched result with a single section. I am able to access the objects using [[fetchedResultsController fetchedObjects] objectAtIndex:index] for all of the objects. But it is failing when I use objectAtIndexPath like this: [fetchedResultsController objectAtIndexpath:indexPath]
An error occurs after I insert an row (for one of the SearchResult objects) into the corresponding table. The object appears to be inserted into the new table correctly. After I have visually confirmed this, I select one of the rows, and then the fun begins.
Here is the code where the error is occurring:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
SearchResult *event;
NSLog(#"Number of sections in fetchedResultsController: %d", [[fetchedResultsController sections] count]);
for (NSUInteger i=0; i<[[fetchedResultsController fetchedObjects] count]; i++) {
event = (SearchResult*)[[fetchedResultsController fetchedObjects] objectAtIndex:i];
NSLog(#"object at index[%d]: %#", i, event.title );
NSLog(#"indexPath for object at index[%d]:%#", i, [fetchedResultsController indexPathForObject:event]);
}
NSLog(#"indexPath passed to method: %#", indexPath);
SearchResult *result = [fetchedResultsController objectAtIndexPath:indexPath]; // *** here is where the error occurs ***
[viewDelegate getDetails:result];
}
I am having a failure in the last line. The log looks like this:
Number of sections in fetchedResultsController: 1
object at index[0]: Cirles
indexPath for object at index[0]:(null)
object at index[1]: Begin
indexPath for object at index[1]:(null)
object at index[2]: Copy
indexPath for object at index[2]:(null)
object at index[3]: Unbound
indexPath for object at index[3]:(null)
indexPath passed to method: <NSIndexPath 0x64ddea0> 2 indexes [0, 2]
After executing the NSLog statements, I get an exception at the last line using [fetchedResultsController objectAtIndexPath:indexPath]. It happens for other index values too, but they always look valid.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no object at index 2 in section at index 0'
So, to summarize, there appear to be the right number of fetched objects, there is one section (0), I can access each one by one method, but not by the other. The indexPathForObject: is always returning (null).
Is this a bug or am I misunderstanding something?
UPDATE
Here is the code, implementing the NSFetchedResultsControllerDelegate protocol methods.
- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"Favorites controllerWillChangeContent");
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"Favorites Table controllerDidChangeContent");
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"Favorites Changed Object");
switch (type) {
case NSFetchedResultsChangeInsert:
NSLog(#"--- Favorite was inserted");
if ([[self.fetchedResultsController fetchedObjects] count] == 1) {
// configure first cell to replace the empty list indicator by first deleting the "empty" row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
case NSFetchedResultsChangeDelete:
NSLog(#"--- Favorite was deleted");
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
if ([[self.fetchedResultsController fetchedObjects] count] == 0) {
// configure first cell to show that we have an empty list
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
break;
case NSFetchedResultsChangeMove:
NSLog(#"--- Favorite was moved");
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"--- Favorite was updated");
[self configureCell:(ResultCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type {
switch (type) {
case NSFetchedResultsChangeInsert:
NSLog(#"Favorites Section Added");
break;
case NSFetchedResultsChangeDelete:
NSLog(#"Favorites Section Deleted");
break;
default:
break;
}
}
UPDATE 2
I don't know if it matters, but the tableView is initialized with this line:
tableView = [[UITableView alloc] initWithFrame:CGRectMake(...) style:UITableViewStyleGrouped];
UPDATE 3
When I change the offending line as follows, it works fine. But I would rather keep the indexing the same as it is with the table.
//SearchResult *result = [self.fetchedResultsController objectAtIndexPath:indexPath];
SearchResult *result = [[self.fetchedResultsController fetchedObjects] objectAtIndex:[indexPath indexAtPosition:1]]

I had the same problem now.
For me it helped to initialise NSFetchedResultsController with cacheName: nil.

Related

TableView sections with NSManaged Object Context Throws Exception

I recently migrated a project from a self-managed object using a config singleton to use the NSManaged Object Context with NSFetchedResultController. What I'm trying to do is fill a TableView with sections that are based on month however the user can select a cell and change the month. when that happens it causes the following exception thrown and the cells become unable to change or edit
[error] error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to insert row 0 into section 1, but there are only 0 sections after the update with userInfo (null)
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to insert row 0 into section 1, but there are only 0 sections after the update with userInfo (null)
Here is the main view controller fetch request:
- (NSFetchedResultsController<Budget *> *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
LogDebug(#"STARTED");
NSFetchRequest<Budget *> *fetchRequest = Budget.fetchRequest;
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
NSFetchedResultsController<Budget *> *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"monthSection" cacheName:nil];
aFetchedResultsController.delegate = self;
NSError *error = nil;
if (![aFetchedResultsController performFetch:&error]) {
LogError(#"Unresolved error %#, %#", error, error.userInfo);
abort();
}
_fetchedResultsController = aFetchedResultsController;
return _fetchedResultsController;
}
Then for the Tableview Sections Data Source:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
NSInteger count = [sectionInfo numberOfObjects];
LogDebug(#"Number of Rows: %ld in Section %ld",(long)count, (long)section);
return [sectionInfo numberOfObjects];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = [[self.fetchedResultsController sections] count];
LogDebug(#"Sections: %ld",(long)count);
return [[self.fetchedResultsController sections] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
LogDebug(#"STARTED");
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController.sections objectAtIndex:section];
return [sectionInfo name];
}
I have also tried subclassing the data object model and added this to setting the month section:
- (NSString *)monthSection {
#synchronized (self.startTime) {
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:#"MMMM"];
NSString *sectionTitle = [formatter stringFromDate:self.startTime];
return sectionTitle;
}
}
Now when the user selects a table view cell I send the Budget Object to the DetailViewController by sending it the following way:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Budget *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController];
[controller setDetailItem:object];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
Then in DetailViewController i just use the setter on the startTime:
[startDatePicker setDate:detailItem.startTime];
[[AppDelegate instance] saveContext];
[self totalUpFields];
But once the user changes the Month date to something other than what was initially created it thrown that above exception.
I'm very new to the NSManaged Object structure and I've always used a managed config singleton.
For the changed object content here are the methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
- (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;
default:
return;
}
}
- (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:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withBudget:anObject];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
}
}
Thanks for the help, let me know if i need to add additional details.
I believe this is an issue when moving the last row out of a section. You can resolve it by changing your case NSFetchedResultsChangeMove: to the following:
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
It looks like this happens because the sections are modified before the rows, so the index paths shift and the expected source/destination may no longer be valid.
It's also important to first sort by your sectionNameKeyPath:
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"monthSection" ascending:NO];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor1, sortDescriptor2]];
You will need to add monthSection as a property on your Core Data model and set it when inserting/updating, instead of the dynamic method you're currently using in your subclass (otherwise you'll get an exception when performing the fetch).

NSFetchedResultsController crashes on objectAtIndexPath?

I'm getting a crash when trying to access an object in NSFetchedResultsController.
2013-11-10 15:15:06.568 Social[11503:70b] CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds for empty array with userInfo (null)
2013-11-10 15:15:06.570 Social[11503:70b] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds for empty array'
viewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
self.resultController = [DataEngine sharedInstance].postsFetchedResultController;
self.resultController.delegate = self;
[self.resultController performFetch:nil];
[self.tableView reloadData];
[[DataEngine sharedInstance] fetchInBackground];
}
Result Controller Delegate
#pragma mark - NSFetchedResultsControllerDelegate Methods -
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
default:
break;
}
}
table view
#pragma mark - UITableView Delegate & Datasrouce -
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.resultController.fetchedObjects.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self cellForRowAtIndexPath:indexPath];
}
One reason for crash on below line could be that you are updating self.resultContrlller after you have used it for retuning the number of rows of your table section. Make sure that if you are constantly updating your self.resultContrlller object then you take a copy of it and then use for your table drawing.
// This code does not prevent crash. Changing your regular code to modern objective-C way
Post *post = [self.resultContrlller fetchedObjects][indexPath.row];
The data source should not change while table is reloading itself. You should use a copy of fetchedObjects instead to load the table. So, evey time, before you reload your table, take a copy [[self.resultContrlller fetchedObjects] copy] and the use it for table drawing. Your main source can then keep on changing. And after table reload is done with copy you may want to reload it it again if there was a change in the data. Such crashes happens when your data source changes faster than table reloads.
In one of my NSManagedObject subclasses I had the follwiing code which was causing the issue. NSSets are automatically initialized on NSManagedObejcts and there is no need to initialize them.
- (void)awakeFromFetch
{
if (!self.comments)
self.comments = [NSMutableSet set];
if (!self.medias)
self.medias = [NSMutableSet set];
}
Another problem was that during insert indexPath is null, I had to use newIndexPath instead
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;

fetchedresultscontroller delegate called with invalid indexpath when 1 managed object updated and another deleted [duplicate]

I have a UITableView that uses an NSFetchedResultsController as data source.
The core data store is updated in multiple background threads running in parallel (each thread using it's own NSManagedObjectContext).
The main thread observes the NSManagedObjectContextDidSaveNotification
notification and updates it's context with mergeChangesFromContextDidSaveNotification:.
Sometimes it happens that the NSFetchedResultsController sends an
NSFetchedResultsChangeUpdate event with an indexPath that does not exist
anymore at that point.
For example: The result set of the fetched results controller contains
1 section with 4 objects. The first object is deleted in one thread.
The last object is updated in a different thread. Then sometimes the
following happens:
controllerWillChangeContent: is called.
controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called with
type = NSFetchedResultsChangeDelete, indexPath.row = 0.
controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called with
type = NSFetchedResultsChangeUpdate, indexPath.row = 3.
But the fetched results controller contains only 3 objects now, and if call
MyManagedObject *obj = [controller objectAtIndexPath:indexPath]
to update the table view cell according to the NSFetchedResultsChangeUpdate
event, this crashes with a NSRangeException exception.
Thank you for any help or ideas!
I have now found a solution for my problem. In the case of an update event, there is no need to call
[self.controller objectAtIndexPath:indexPath]
because the updated object is already supplied as the anObject parameter to the -controller:didChangedObject:... delegate.
I have therefore replaced -configureCell:atIndexPath: by a -configureCell:withObject: method that uses the updated object directly. This seems to work without problems.
The code looks now like this:
- (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;
/* ... */
}
}
This is actually quite common because of the bug in Apple's boiler plate code for NSFetchedResultsControllerDelegate, which you get when you create a new master/detail project with Core Data enabled:
- (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 query the fetched results controller and risk using an incorrect index path when the object is already given to you? Martin R recommends this solution as well.
Simply change the helper method configureCell:atIndexPath: from taking an index path to take in the actual object that was modified:
- (void)configureCell:(UITableViewCell *)cell withObject:(NSManagedObject *)object {
cell.textLabel.text = [[object valueForKey:#"timeStamp"] description];
}
In cell for 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
As of iOS 7.1, both indexPath and newIndexPath are passed in when a NSFetchedResultsChangeUpdate happens.
Simply keep the default implementation's usage of indexPath when calling cellForRowAtIndexPath, but change the second index path that is sent in to newIndexPath:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:newIndexPath];
break;
Solution #3: Reload rows at index path
Ole Begemann's solution is to reload the index paths. Replace the call to configure cell with a call to reload rows:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
There are two disadvantages with this method:
By calling reload rows, it will call cellForRow, which in turn calls dequeueReusableCellWithIdentifier, which will reuse an existing cell, possibly getting rid of important state (e.g. if the cell is in the middle of being dragged a la Mailbox style).
It will incorrectly try and reload a cell that isn't visible. In Apple's original 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 with indexPathsForVisibleRows before calling reload rows.
Reproducing the bug
Create a new master/detail project with core data in Xcode 6.4.
Add a title attribute to the core data event object.
Populate the table with several records (e.g. 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 configure 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 a record when the new button is tapped, update the last item (Note: this can be done before or after the item is created, but make sure to do it before save is called!):
// update the last item
NSArray *objects = [self.fetchedResultsController fetchedObjects];
NSManagedObject *lastObject = [objects lastObject];
[lastObject setValue:#"updated" forKey:#"title"];
Run the app. You should see five items.
Tap the new button. You will see that a new item is added to the top, and that the last item does not have the text "updated," even though it should have had it. If you force the cell to reload (e.g. by scrolling the cell off the screen), it will have the text "updated."
Now implement one of the three solutions outlined above and in addition to an item being added, the last item's text will change to "updated."

implementing NSFetchedResultsController

I'm having some problems implementing NSFetchedResultsController for something very simple. I'm just trying to learn how it works. All I want to do is display the firstName attribute of Person entities in a table view.
As far as I know I've implemented all the methods required but nothing is showing up in the table view. The method -tableView:cellForRowAtIndexPath: method isn't even being called.
Here is my code:
#implementation MyTableViewController
#synthesize managedObjectContext = _managedObjectContext;
#synthesize fetchedResultsController = _fetchedResultsController;
-(NSFetchedResultsController*) fetchedResultsController
{
if (!_fetchedResultsController)
{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"firstName" ascending:YES];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
}
return _fetchedResultsController;
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSError *error = nil;
[self.fetchedResultsController performFetch:&error];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
Person *person = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = person.firstName;
return cell;
}
any help would be hugely appreciated! many thanks, Alex
You have to call [NSFetchedResultsController performFetch:] and implement its delegate
- (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:[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)controller:(NSFetchedResultsController *)controller didChangeSection:(id )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;
}
}
- (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];
}
See more at: http://www.raywenderlich.com/999/core-data-tutorial-for-ios-how-to-use-nsfetchedresultscontroller
Try getting your NSString from your Person object and via the code here:
NSString *firstName = ((Person *)[personArray objectAtIndex:indexPath.row]).firstName;
Mind you, my suggested line is assuming that you output your NSFetchRequest to a NS(Mutable)Array called personArray.
Hopefully that helps, otherwise let me know.

Crash on UITableView endUpdates when moving last row in section

I have an UITableViewController which is backed by an NSFetchedResultsController.
My NSFetchedResultsController put results into two sections based on a boolean.
In a background thread, the datasource is altered such that rows are added or removed.
I have my background thread's NSManagedObjectContext's merging correctly and this situation work fine for most cases. Rows alter when the data is changed and move between sections with animations.
There is one situation however where my application crashes with an EXC_BAD_ACCESS.
The case is when the last row in a section is moved to the other section. (Stack trace below). The crash occurs in objc_msgSend but the normal debugging tips I use aren't returning anything helpful, I simply receive "Value can't be converted to integer" from gdb.
The NSFetchedResultsControllerDelegate methods get called in the following order:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
// ^ The parameters specify a deletion of the 1st section
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
// ^ The parameters specify a moved row from the 1st section to the 1st section
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
Looking through StackOverflow, many of the people who encounter similar issues are backing their UITableViewController manually using an NSArray or NSMutableArray and they're simply not updating the datasource at the correct time. In this case the NSFetchedResultsController is handling returning the number of rows and sections and is definitely updated by the time the [tableView endUpdates] is called.
Has anyone got any debugging tips, pointers or solutions?
This blog post hints at working around a similar problem where a new section is created.
I have two options if I cannot solve this issue as is:
Forgo animation on rows and simply call [tableView reloadData]
Switch to using NSArray backed storage for the UITableView and hope for the best
Update
I have modified the Apple sample Core Data application "Locations" to use an NSFetchedResultsController directly and reproduced the issue.
I have uploaded the source code for this project. Use that project to reproduce the issue.
Simply click the + button a few times and wait
Every 5 seconds an item gets moved to another category.
When the first category is about to be emptied, it crashes.
The crash is sometimes about creating two animations for the same cell as per other StackOverflow questions on the topic. More commonly the crash is as discussed above and below.
Reference
The stack trace:
#0 0x31e0afbc in objc_msgSend ()
#1 0x32c11522 in -[_UITableViewUpdateSupport(Private) _computeRowUpdates] ()
#2 0x32c10510 in -[_UITableViewUpdateSupport initWithTableView:updateItems:oldRowData:newRowData:oldRowRange:newRowRange:context:] ()
#3 0x32c0f99e in -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] ()
#4 0x32c0e66c in -[UITableView endUpdatesWithContext:] ()
#5 0x000088d6 in -[DraftHistoryController controllerDidChangeContent:] (self=0x3f6f1e0, _cmd=0x377ca29c, controller=0x3f716e0) at /Users/ataylor/Documents/Documents/Programming/iPhone/Drafter/Drafter/Drafter/DraftHistoryController.m:323
#6 0x3775a892 in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()
My NSFetchedResultsControllerDelegate methods are the same ones from the Apple sample code:
- (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:[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];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
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:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
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]; // <---- Crash occurs here
}
The relevant UITableViewControllerDelegate methods are as follows:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
I took a quick look. Your modified Locations project didn't crash on me but it did generate core data exceptions. The problem lies in reloading the sections in controller:didChangeObject:. I changed the code as follows and everything looks good to me (including the section titles) on both iOS 4.3 and iOS 5.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
// other cases here
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I was super curious about the code that fixed an issue with adding a new section so I decided to pop it in place of my current NSFetchedResultsControllerDelegate methods and it indeed solved my problem. Tracing through, I have discovered that this code from the Apple "Locations" sample crashes when using the NSFetchedResultsController:
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
But the following code works perfectly:
case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationTop];
}
else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}
break;
I don't have a case where section headings will change, I won't continue debugging to solve the issue Apple's code purports to fix around section titles.
Because every question needs a swift answer, here's one (based on XJones's accepted answer). As is the case with the other answers, the magic is in the .Move: delete and insert instead of moving the row.
This is my NSFRC delegate didChangeObject:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Delete:
if let deletePath = indexPath {
self.tableView.deleteRowsAtIndexPaths([deletePath], withRowAnimation: .None)
}
break
case .Insert:
if let insertPath = newIndexPath {
self.tableView.insertRowsAtIndexPaths([insertPath], withRowAnimation: .Fade)
}
break
case .Update:
if let updatePath = indexPath {
self.tableView.reloadRowsAtIndexPaths([updatePath], withRowAnimation: .None)
}
break
case .Move:
if let indexPath = indexPath, newIndexPath = newIndexPath {
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break
}
}
I was getting a crash overtime I deleted the last row in a section.
Do make sure you have implemented controller:didChangeSection:atIndex:forChangeType: to handle section removal.
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.viewTable insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.viewTable deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}