2
votes

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.

3
Are you sure your delegate is set? Not totally sure I'm following your sample run but the fetch controller won't track changes if there isn't one set and implemented. - Ben Zotto
Note that you cannot use block based sort descriptors in a (SQlite based) Core Data fetch request. - Perhaps you can show your code for creating the sort descriptors, fetch request and fetched results controller. - Martin R
@BenZotto the delegate is definitely set. To try to fix the issue I changed it to unset it and re-set it on viewWillDisappear/viewWillAppear, respectively, but it makes no difference. Note that the newly created and saved item DOES appear in the list at the right point. The problem only starts when I go Back then forward again to redisplay the list - Rhubarb
@tc the save happens in a block in the TVC right after the New Item TVC closes when the user hits Save. ie. I have a static TVC for creating new item that displays modally with a Save button and pressing it calls a block on the parent TVC which saves, then pops the TVC. Standard stuff. It is worth noting that I'm using a 2nd temporary MOC for my new item. When I save it, I save the temporary MOC, then the parent MOC, then I set the position and save the parent MOC again. - Rhubarb
@MartinR Weird. Why would blocks be disallowed? As it happens I did use a block trying to debug the issue - but that was just in case CoreData couldnt naturally sort on an NSNumber column. I'm pretty sure it can. (It works every time but the first time) - Rhubarb

3 Answers

0
votes

I'm posting my "solution" here, but I won't accept this as an answer for a while, in case it turns out that it's my use of the NSFetchedResultController that leads to sorting to be wrong, and not just a bug. (See comments under the question)

In the end, I cheat by getting all of the fetched objects and looking up the one at the position I want. This won't work for all cases: for one it's easy for me because I'm sorting on an integer, and I know the list is up to date (it's not being filled in the background) and small so there's not a big performance problem with looking through all the objects.

Specifically: I replaced the typical line in cellForRowAtIndexPath that looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ....
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
    ...

with this

{
    ...
    Item *item = [self _lookupItemAtIndexPath:indexPath];
    ...

which is implemented here

- (Item *)_lookupItemAtIndexPath:(NSIndexPath *)indexPath
{
    // Since the FRC is sorted on position, _theoretically_ this next call should just
    // return the right item, but the damn sorting doesnt always work (esp not after
    // a new addition), so we check if it's the one we expect and look it up if not
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if (item.position.integerValue != indexPath.row) {
        // the sorting failed! so do a lookup to find the item with the right position
        // (Note that its crucial that the positions remain contiguous and up-to-date)
        NSArray *items = self.fetchedResultsController.fetchedObjects;
        NSUInteger index = [items indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
            if (((Item *)obj).position.integerValue == indexPath.row) {
                // found it - stop looking and return YES
                *stop = YES;
                return YES;
            }
            else {
                return NO;
            }
        }];

        NSAssert1(index != NSNotFound, @"Failed to find the item with the right position: %d", indexPath.row); 

        item = (Item *)[items objectAtIndex:index];

        // Temporary log for debugging: tells me how often the sort is actually failing
        NSLog(@"Warning: Item %@ was out of place, found it at %d instead of %d", item.title, index, indexPath.row);
    }
    return item;
}

Note that I'm trying the original lookup first and testing if its correct so I don't do my "slow" lookup if its unnecessary. Most of the time it is _un_necessary - it only seems to be a problem right after adding something. (I can tell that from the NSLog message in there)

Again, this is not really an answer to the failure of the sort - just my workaround for my specific case - so I'm not checking it as an answer (yet)

0
votes

I had same sort problems with CS193P sample code. The solution for me was to save parent context too (after object creation/modification):

NSError* err;
if ([txt.managedObjectContext save:&err]) {
    if ([txt.managedObjectContext.parentContext hasChanges]) {
        if ([txt.managedObjectContext.parentContext save:&err]) {
            NSLog(@"parent context save ok");
        } else {
            NSLog(@"can't save parent context; error: %@", err);
        }
    }
} else {
    NSLog(@"can't save text; error: %@", err);
}

Looks like "bug" appears if you are use new UIDocument method instead of old Core Data template with manual set up of NSManagedObjectContext, NSPersistentStoreCoordinator and NSManagedObjectModel.

0
votes

OK, so I did some tooling about. I tried to reproduce the problem in a dummy project and couldn't. I think this probably means the bug is in my code, not Core Data. Its just too damned hard to find.

So since I know the problem "disappears" when the updates are actually saved, I looked into how to force a save. The Apple doc says you should not save the NSManagedObjectContext or its parent because "you sidestep other important operations that the document performs". However, this is the only way I have found to force a save (just saving the moc or using updateChangeCount don't actually result in SQL being execute right now). I don't want to upvote the response from zxcat because I don't know what is being "sidestepped", but it seems to work.

UPDATE: Overnight I have had a further think about it and decided that whatever I am doing "wrong", there must still be a bug in NSFetchedResultsController. This is because it can end up in an illogical state - ie. the contents of fetchedObjects does not match the contents of sections. In all cases, fetchedObjects is always correct. In some cases, like mine and the OP, sections is reflecting the current persisted state, rather than the in-memory state.