8

I am currently trying to populate a UITableView in my project from Core Data using NSFetchedResultsController. I am using a custom search with a comparator (although I have also tried a selector and had an identical problem):

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
     Set up the fetched results controller.
    */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"objectName" ascending:YES comparator:^(id s1, id s2) {
            NSLog(@"Comparator");
      //custom compare here with print statement
    }];
    NSLog(@"Sort Descriptor Set");
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    [fetchRequest setSortDescriptors:sortDescriptors];

    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"firstLetterOfObject" cacheName:@"Objects"];
    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];
    if (![fetchedResultsController performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return fetchedResultsController;

When I enter this tab, I have logged all over the program and found that the NSFetchedResultsController does not even enter the comparator block when fetching. It instead sorts it with some default sorting method.

If I delete and add an Object with an objectName, however, it then does enter the comparator block and correctly sort the table.

Why does the NSFetchedResultsController not sort using the comparator until the managed object model is changed?

Notes: I have tried also turning off caching, and/or performing a fetch in viewDidLoad:, but it seems that how many times I fetch does not matter, but when. For some reason it only uses my sorting after the object model has been changed.

Matthew Bischoff
  • 1,043
  • 11
  • 27

3 Answers3

8

There are a couple of things I can think of. First, though this may not be your problem, you cannot sort on transient properties. But more likely is that when sorting in a model backed by a SQL store, the comparator gets "compiled" to a SQL query, and not all Objective-C functions are available. In this case, you'd need to sort in memory after the fetch is performed.

EDIT: See this doc, specifically the Fetch Predicates and Sort Descriptors section.

Don
  • 3,654
  • 1
  • 26
  • 47
  • 1
    They are not transient properties, but your other suggestion definitely seems like it could be the problem. The only problem is that I am relying on the fetchedresultscontroller to provide the section names and such to the table view, so sorting in memory every time would be difficult (I think). How would you recommend going about it? And my other question would be why does the comparator block work after the object model has been changed? Thank you! – Matthew Bischoff Jan 25 '11 at 16:32
  • Well actually the more general problem here is that I am implementing sections into my table view, along with a section index. I put any Object starting with a number in a "123" section (this works), but I want this section to be at the bottom like the iPod app. According to Apple's apps, the numbers should be the last option in the sectionIndex (I'm using UILocalizedIndexedCollation to generate the section indexes). There seems to be no way, however, to fetch the sections so that numbers come last. Do you know of a more reliable way to do this than custom sorting? – Matthew Bischoff Jan 25 '11 at 16:50
  • 2
    Just a guess, but I think the comparator block works after the object model has been changed because the graph is already in memory. But that's just a guess. The way you'd sort manually, I believe, is to subclass NSArrayController, so you'd lose the NSFetchedResultsController benefits. But, as you say, the root problem is that you want to sort numbers to the bottom. Off the top of my head, I'd say create a sort order property on your objects and use that, but I haven't done enough custom sorting using sort descriptors. – Don Jan 25 '11 at 17:16
  • Thanks again Don, it seems like you were spot on with that find in the documentation. I also got this working by loading everything into an array and working with that to populate the list and determine the sections, per another stackoverflow question I found. Everything is working great. – Matthew Bischoff Jan 25 '11 at 23:09
  • Great! Glad you got it working. I found this as well on Cocoa Is My Girlfriend, which also suggests using a sortOrder property: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/ – Don Jan 25 '11 at 23:27
1

I see the same problem and a way to work around it is to modify an object, save the change then restore it to its original value and save again.

    // try to force an update for correct initial sorting bug
NSInteger count = [self.fetchedResultsController.sections count];
if (count > 0) {
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:0];
    count = [sectionInfo numberOfObjects];
    if (count > 0) {
        NSManagedObject *obj = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
        NSString *name = [obj valueForKey:@"name"];
        [obj setValue:@"X" forKey:@"name"];
        // Save the context.
        [self saveContext];
        [obj setValue:name forKey:@"name"];
        // Save the context.
        [self saveContext];
    }
}
erne
  • 19
  • 1
0

Sorry, but did you miss the final fetch part to your code snippet?:

NSError *error;
BOOL success = [aFetchedResultsController performFetch:&error];

Don't forget to release the request too:

[fetchRequest release];
petert
  • 6,672
  • 3
  • 38
  • 46