Stephen Furlani
Stephen Furlani

Reputation: 6856

NSFetchedResultsController returning wrong NSFetchedResultsChangeType

I'm creating an NSFetchedResultsController with the following Fetch Request in viewDidLoad

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Person class])];
fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(sortDate))
                                                               ascending:NO]];

NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                             managedObjectContext:self.mainManagedObjectContext
                                                                               sectionNameKeyPath:nil
                                                                                        cacheName:nil];
controller.delegate = delegate;
NSError *error;
if (![controller performFetch:&error]) {
    NSLogError(error);
}

// Connecting up all the delegates for UITableView

And reloading it in viewWillAppear with [self.tableView reloadData]

A simplified Person

@interface Person : NSManagedObject
/// an NSTimeInterval representing the Unix Epoch
@property (nonatomic, retain) NSNumber *sortDate;
- (void)viewed;
@end

@implementation Person
// nothing fancy or custom - no extra KVO or primitive assignments
@dynamic sortDate;
- (void)viewed {
    self.sortDate = @([NSDate date].timeIntervalSince1970);
}
@end

My data is generated with a network request to fetch a bunch of json and convert it into NSManagedObjects. The sorting looks fine.

1 Person A time 1437498898
2 Person B time 1437498897
3 Person C time 1437498896

When I adjust the sortDate property on Person C with the -viewed method, in the <NSFetchedResultsControllerDelegate> I get a change type of NSFetchedResultsChangeMove and the list is sorted like this:

1 Person C time 1437498900
2 Person A time 1437498898
3 Person B time 1437498897

When I attempt to do the same for Person B I instead get a change type of NSFetchedResultsChangeUpdate and the list is sorted like this:

1 Person C time 1437498900
2 Person A time 1437498898
3 Person B time 1437498905

I can continuously update the sortDate for Persons A and C and they'll continue to sort correctly. Any changes to Person B don't show until after the app is rebooted but then Person B stops updating. Deleting and reinstalling my app (to clear CoreData) doesn't help.

If I place a print command in the - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath then I can see that Person B's sortDate has indeed updated to the new value, which should cause the NSFRC to create a Move event instead of Update change type.

// Original Sort (logs from cellForRowAtIndexPath)
Time 1437506506 - Person B
Time 1437506490 - Person C
Time 1437506381 - Person D
Time 1437506128 - Person A

// Change Person A
Updating SortDate 1437506128 -->  1437506599
Time 1437506599 - Person A for NSFetchedResultsChangeMove

// Sorted Correctly
Time 1437506599 - Person A
Time 1437506506 - Person B
Time 1437506490 - Person C
Time 1437506381 - Person D

// Change Person B
Updating SortDate 1437506506 -->  1437506610
Time 1437506610 - Person B for NSFetchedResultsChangeUpdate

// Sorted Incorrectly
Time 1437506599 - Person A
Time 1437506610 - Person B
Time 1437506490 - Person C
Time 1437506381 - Person D

So, why is the NSFetchedResultsController returning the wrong NSFetchedResultsChangeType?

Upvotes: 3

Views: 498

Answers (1)

Mundi
Mundi

Reputation: 80265

From the documentation of NSFetchedResultsControllerDelegate:

A move is reported when the changed attribute on the object is one of the sort descriptors used in the fetch request.

An update of the object is assumed in this case, but no separate update message is sent to the delegate.

An update is reported when an object’s state changes, but the changed attributes aren’t part of the sort keys.

I hope this makes it clear why you get different change types in your callback. Considering this, I would advise to clean up your fetched results controller and delegate callbacks:

  • Eliminate the redundant predicate in the FRC
  • Use the sort key the corresponds to the attribute name: "sortDate".
  • Make sure that the delegate callback method moves the table view cell to the new index path, i.e. by calling deleteRowsAtIndexPaths and insertRowsAtIndexPaths as appropriate.

Final thought: the erratic behavior you describe for one particular objects (while the others seem to work as expected) points to other possible errors, e.g in connection with updating the sortDate attribute.

Also, you did not explain the keypath variable in your fetched results controller. Maybe the grouping is contributing to the behavior you are observing.

Finally, it is possible that you are the victim of a rare malfunction of the fetched results controller delegate described here (including workaround). - Also, don't forget that if you do not need to animate the changes, you can always just invalidate the FRC and call reloadData.

If nothing helps I would refactor with NSDate instead of NSNumber which is anyway more intuitive.

Upvotes: 5

Related Questions