I have a pretty standard app with a UITableViewController that extends CoreDataTableViewController from the CS193P stanford class (which is simply an extension of UITableViewController implementing NSFetchedResultsControllerDelegate with all of the boilerplate code from the NSFetchedResultsControllerDelegate documentation).
Anyway, my table shows a list of items. The items have a property called position which is an
integer (an NSNumber in Core Data) and the NSFetchedResultsController is set up with an
NSSortDescriptor to sort on position.
This usually works: when my table opens a performFetch is done and the items come in the right order. I added some logging messages to debug.
fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
fetched item: aaaaa has array:pos = 0 : 0
fetched item: bbbb has array:pos = 1 : 1
fetched item: cccc has array:pos = 2 : 2
fetched item: dddddd has array:pos = 3 : 3
What the first line says is that the performFetch is occuring with a predicate filtering on GUID and a sort descriptor sorting on position. The next lines are logged when looping over fetchedObjects from the NSFetchedResultsController after the fetch. The item name is shown first (aaaa, bbbb, etc) then the array position in the fetchedObjects array, then the value of the position property. You can see how they're all lined up.
The trouble comes when I add a new item, then go back to the parent view, then forward to the list again. The new item gets added with the right position (the end). But when I go back and forward a couple of the items are out of order.
At first I thought maybe the fetch wasn't being performed again or the sortDescriptor was missing but logging shows that the fetch is occurring and you can see things are out of order.
fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
fetched item: bbbb has array:pos = 0 : 1 <= BAD
fetched item: cccc has array:pos = 1 : 2 <= BAD
fetched item: aaaaa has array:pos = 2 : 0 <= BAD
fetched item: dddddd has array:pos = 3 : 3
fetched item: eeee has array:pos = 4 : 4
See there: note how array item 0 has position 1 and array item 2 has position 0 and 1 has 2! Since this is actually the fetched objects immediately after the fetch, and since the fetchRequestcontroller's sortDescriptor and predicate are clearly correct, how is this even possible?
At first I thought it might be an issuein the table view, but then I added this debug logging after teh fetchObjects so I know it's the results of the fetch.
I also considered that maybe NSNumbers can't be sorted automatically, so I added my own comparator to sort on integer values. But no difference.
Note that if I go back and forward again, the next fetch will put things back in the right order. So will all subsequent fetches. It's only this one that happens after loading.
Any ideas?
[UPDATE]
After some helpful discussion in the comments (thanks @MartinR and @tc for taking an interest) I've simplified things a bit and added some code to demonstrate what's happening. Simplifications:
- I'm now sorting on Item "title" because its a simple NSString.
- I no longer use a child NSManagedObjectContext for creating new items - they're created directly in the same MOC as the list and saved immediately (and synchronously)
Adding some code to demonstrate: The basic set up is a list of lists of items. Standard Todo-app stuff. So my CoreData Model contains lists and items. Every list has a set of items (1-many) and every item has a reference back to its parent list.
The UI is 2 TVCs: ListOfListsTVC, click on a list name and it segues to ListOfItemsTVC
Now since the list of items is used for different lists, it sets up a completely new FRC each time a new list is set. That happens here:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"item"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"parentList.guid = %@", self.list.guid];
request.predicate = predicate;
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.list.managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
self.debug = YES;
}
That self.fetchedResultsController calls into the CoreDataTableViewController superclass which is verbatim from the cs193p Stanford course:
- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc
{
self.debug = YES;
NSFetchedResultsController *oldfrc = _fetchedResultsController;
if (newfrc != oldfrc) {
_fetchedResultsController = newfrc;
newfrc.delegate = self;
if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) {
self.title = newfrc.fetchRequest.entity.name;
}
if (newfrc) {
if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
[self performFetch];
} else {
if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
[self.tableView reloadData];
}
}
}
It's mostly debug info, but the key statements in there are: 1. setting the property; 2. setting the delegate of the FRC to be this TVC (self) and 3. doing an immediate fetch.
performFetch is in the same CoreDataTableViewController class and dumps all of that debug info I listed above: the name of the item, the position in the fetchedObjects array and the value of the position. (It's actually a generic method that fetches lists too, but I put a test in for the Item class to get that extra debug info)
I wont list it here, but the key statements are:
NSError *error;
[self.fetchedResultsController performFetch:&error];
// Debug:
NSArray *obs = [self.fetchedResultsController fetchedObjects];
// log all the debug info about the items in the fetched array to prove they're not sorted
// ...
[self.tableView reloadData];
So basically, fetch and reload the table.
This seems to work when I first start the app if there's a list of items in the data. Where it seems to fail is after I add a new item. I do that in a separate static TVC called NewItemTVC and instead of a delegate I use a callback in a block to save the item. But the effect is the same: it's all synchronous. Here's my block for saving in the ListOfItemsTCV
newItemTVC.saveCancelBlock2 = ^ (BOOL save, NSDictionary *descriptor) {
if (save) {
NSManagedObjectContext *moc = self.fetchedResultsController.managedObjectContext;
Item *newItem = [Item itemWithDescriptor:itemDescriptor inManagedObjectContext:moc];
// here's where I set the position but ignore this for now because
// I'm sorting on "title" to debug and it has the same problem
NSInteger newPosition = self.list.lastPosition + 1;
newItem.position = [NSNumber numberWithInteger:newPosition];
// and finally add it to the list
[self.list addItemsObject:newItem];
NSError *error = nil;
BOOL saved = [moc save:&error];
if (!saved) {
NSLog(@"Unresolved error saving after adding item to parent %@, %@", error, [error userInfo]);
}
}
Now, after I save the item, the NewItemTVC pops and the ListOfItems reloads, performs the fetch, and sometimes has the correct order, usually not. The fetch is performed in this case in viewWillAppear. (It didnt used to be but I've added this while debugging too. Now viewWillDisappear sets the delegate to nil, and popping the NewItemTVC causes this code to do a new fetch after setting the delegate of the FRC back to the TVC) Note also that this doesn't set teh delegate or perform the fetch when coming forward from the list of lists, because setting the list property already does that (sets the delegate and performs the fetch). So in fact, this returning from popping the NewItemTVC and performing fetch in the viewWillAppear is the first instance where sorting appears wrong.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.fetchedResultsController != nil && self.fetchedResultsController.delegate != self) {
self.fetchedResultsController.delegate = self;
[self performFetch];
[self.tableView reloadData];
}
}
where it really goes wrong is when I then hit Back to see my ListOfListsTVC, then hit the list again to back into the same ListOfItemsTVC (it doesnt matter if I have 1 list or a dozen). The first time I do this the items are always out of order. Sometimes I can repeat it 4 or 5 times and they'll still be out of order, but eventually after a number of back and forwards, they get into order and stay that way.
Here's the (sanitized) debug info now that I'm using the "title" of my Item, instead of the position.
[808:fb03] [ListOfItemsTVC performFetch] fetching Item with pedicate: parentList.guid == "DD1E1F25-BFC9-46B9-A637-109C0D6F0D1D" and sort: (title, ascending, compare:)
[808:fb03] fetched item: ccccc in array at index 0
[808:fb03] fetched item: aaaaa in arrat at index 1
[808:fb03] fetched item: bbbbb in array at index 2
a few back-forwards later it settles down to aaaa, bbbb, ccccc - the correct order. It seems to me that sort is just broken or FRC is.