LavaSlider
LavaSlider

Reputation: 2504

NSFetchedResultsController corrupted sort after saving one of the listed objects

I have a generic set of views/classes for CoreData browsing but am having trouble with the sort order for my fetched results controller after having saved a change to an attribute of one of the listed objects.

In viewWillAppear: of my table view controller I set up my fetched results controller as such:

- (void) setupFetchedResultsController {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: self.entityToList];
    request.predicate = self.entitySelectionPredicate; // Typically nil
    request.sortDescriptors = self.entitySortDescriptorList;
    self.fetchedResultsController = [[NSFetchedResultsController alloc]
                 initWithFetchRequest: request 
                 managedObjectContext: self.contextForEntity
                 sectionNameKeyPath: self.keyPathForSections
                 cacheName: nil]; /* Not chacheing */
}

In didSelectRowAtIndexPath for this tableview controller I push to a detail table view controller like this:

- (void) tableView: (UITableView *) tableView didSelectRowAtIndexPath: (NSIndexPath *) indexPath {
    id objectInCell = [self.fetchedResultsController objectAtIndexPath: indexPath];
    ManagedObjectDetailTableViewController *dvc = [[ManagedObjectDetailTableViewController alloc]
                    initWithStyle: UITableViewStyleGrouped];
    dvc.detailItem = objectInCell;
    [self.navigationController pushViewController: dvc animated: YES];
}

The ManagedObjectDetailTableViewController has a row for each attribute and each relationship. In didSelectRowAtIndexPath I push to a ManagedObjectAttributeEditViewController view controller if a row containing and attribute is selected:

- (void) tableView: (UITableView *) tableView didSelectRowAtIndexPath: (NSIndexPath *) indexPath {
    // Section 0 has the attributes for the 'detailItem' object alphabetically by name
    if( indexPath.section == 0 ) {
        ManagedObjectAttributeEditViewController *evc = [[ManagedObjectAttributeEditViewController alloc]
                    initWithNibName: @"ManagedObjectAttributeEditViewController" bundle: nil];
        evc.editedObject = self.detailItem;
        evc.delegate = self;
        // Figure out from the row which attribute was selected
        NSEntityDescription *entity = self.detailItem.entity;
        NSDictionary *attributes = entity.attributesByName;
        NSArray *keys = [attributes allKeys];
        keys = [keys sortedArrayUsingSelector: @selector(compare:)];
        NSString *key = [keys objectAtIndex: indexPath.row];
        evc.editedFieldKey = key;
        [self.navigationController pushViewController: evc animated: YES];
    // The other sections are the relationships for the 'detailItem' object
    } else {
        // Code omitted as not relevant for the error.
    }
}

The ManagedObjectAttributeEditViewController has text fields, etc., to allow editing of the attribute's value. When it's save button is touched is executes:

- (IBAction) save {
    id valueFromView;
    NSAttributeType type = [self typeForEditedAttribute];
    switch( type ) {
    case NSDateAttributeType:
        valueFromView = self.datePicker.date;
        break;
    case NSStringAttributeType:
        if( [self.fieldKeyTester shouldUseTextViewForKey: self.editedFieldKey inEntity: self.editedObject.entity.name] ) {
            valueFromView = self.textView.text;
        } else {
            valueFromView = self.textField.text;
        }
        break;
    case NSInteger16AttributeType:
    case NSInteger32AttributeType:
    case NSInteger64AttributeType:
        valueFromView = [NSNumber numberWithInteger: [self.textField.text integerValue]];
        break;
    case NSDecimalAttributeType:
    case NSDoubleAttributeType:
    case NSFloatAttributeType:
        valueFromView = [NSNumber numberWithDouble: [self.textField.text doubleValue]];
        break;
    case NSBooleanAttributeType:
        valueFromView = [NSNumber numberWithBool: self.switchControl.isOn];
        break;
    case NSObjectIDAttributeType:
    case NSTransformableAttributeType:
    case NSBinaryDataAttributeType:
    case NSUndefinedAttributeType:
        NSLog( @"Don't know how to handle attribute type: %d in %s", type, __func__ );
        break;
    default:
        NSLog( @"Unrecognized attribute type: %d in %s", type, __func__ );
        break;
    }
    [self.delegate managedObjectAttributeEditViewController: self
            didSaveValue: valueFromView forKey: self.editedFieldKey];
}

The ManagedObjectDetailTableViewController is set as the delegate and the didSaveValue:forKey: method is:

- (void) managedObjectAttributeEditViewController: (ManagedObjectAttributeEditViewController *) controller didSaveValue: (id) value forKey: (NSString *) key {
    if( value && key ) {
        [self.detailItem setValue: value forKey: key];
        NSError *error;
        if( ![self.detailItem.managedObjectContext save: &error] ) {
            // Update to handle the error appropriately.
            NSLog( @"Unresolved error doing save of attribute %@.\n%@", key, error.localizedDescription );
        } else {
            NSLog( @"-- successfully saved" );
        }
    } else {
        NSLog( @"Got a cancel from edit attribute" );
    }
    // OK, the attribute editing view controller has told us it is done, pop it
    [self.navigationController popViewControllerAnimated: YES];
}

So, if I am starting with the list of objects for the entity, and they are correctly sorted. I touch a row and it pushes to the ManagedObjectDetailTableViewController. I touch an attribute row in that and it pushes to the ManagedObjectAttributeEditViewController. I change the value and touch save. This pops to the ManagedObjectDetailTableViewController, where everything looks fine. I then touch the back button to go back to the list of objects for the entity but now they are no longer sorted (they seem to always be in the same order but I don't recognize a pattern to the order).

If I count to 10 after doing the save and before touching the back button, the list is properly sorted.

If I comment out the [self.detailItem.managedObjectContext save: &error] method call in didSaveValue:forKey: then the list of objects for the entity remain correctly sorted but if I exit the application before an autosave has occurred I lose the changes.

This makes me think it has something to do with the [self.detailItem.managedObjectContext save: &error] having not completed and the fetched results controller (which is using the same NSManagedObjectContext) for some reason not being able to retrieve the data sorted.

The attribute whose value I am changing is not involved in the sort descriptors, so the order displayed should be the same before and after I revise the value. My database is quite large and it may take a few seconds to write it to disk. I am seeing the problem with iOS 5.1 in the simulator and on a device.

Has anyone ever experienced anything like this or have a suggestion?

Sorry this is so long winded and happy 4th of July for all those Stackoverflowers in the USA!


Revised ManagedObjectDetailTableViewController delegate method didSaveValue:forKey: method is:

- (void) managedObjectAttributeEditViewController: (ManagedObjectAttributeEditViewController *) controller
             didSaveValue: (id) value forKey: (NSString *) key {
    if( value && key ) {
        [self.detailItem setValue: value forKey: key];
        NSError *error;
        if( ![self.detailItem.managedObjectContext save: &error] ) {
            // Update to handle the error appropriately.
            NSLog( @"Unresolved error doing save of attribute %@.\n%@", key, error.localizedDescription );
        } else {
            NSLog( @"-- successfully saved" );
            if( [self.detailItem.managedObjectContext.parentContext.hasChanges] ) {
                if( ![self.detailItem.managedObjectContext.parentContext save: &error] ) {
                    NSLog( @"Unresolved error doing save of parent context for attribute %@.\n%@", key, error.localizedDescription );
                } else {
                    NSLog( @"-- successfully saved the parent context too!" );
                }
            }
        }
    } else {
        NSLog( @"Got a cancel from edit attribute" );
    }
    // OK, the attribute editing view controller has told us it is done, pop it
    [self.navigationController popViewControllerAnimated: YES];
}

I now understand that this double save is necessary for the changes to make it to the permanent store with the latest iOS since with a parent context the save only goes up one level. I don't understand why not having the save propagated to the permanent store should scramble the sort order. Maybe there is some other bug somewhere in my code that this is masking or maybe it is just the way it works...

Upvotes: 3

Views: 447

Answers (1)

Jesse Rusak
Jesse Rusak

Reputation: 57168

OK, based on your edits which include the new stack traces:

You're using UIManagedDocument to manage your core data stack. This means the first save is probably from the UIDocument's main thread context and the second is from its background context. Can you confirm that you're using objects from the UIManagedDocument's managedObjectContext context (as opposed to some other context you're making yourself)?

Another thing. Can you confirm that entitySortDescriptorList is not becoming nil somehow in you setupFetchedResultsController method?

Finally, as other diagnostics:

  • Try calling save: from your top level view controller and see what happens.
  • What happens if you make a delegate for your NSFetchedResultsController? Do you get change notifications? Do they seem reasonable?

Upvotes: 1

Related Questions