user330739
user330739

Reputation: 455

Custom Selector For NSSortDescriptor and NSFetchedResultsController

I am running into a bit of trouble and I was wondering if someone could provide some guidance.

I am attempting to create section headers with the following order: ["Push", "Busy", "Finished", "Canceled"].

Unfortunately, this order cannot be realized with a simple "ascending YES/NO" ... I have to add a custom compare selector ... something like this ..:

**Where "states" maps to ["Push", "Busy", "Finished", "Canceled"]

NSSortDescriptor *sortStates = [NSSortDescriptor sortDescriptorWithKey:@"states"
                                                           ascending:NO 
                                                            selector:@selector(compareCustom:)];

Therefore, I implemented a "compareCustom" ... however, it turns out that I cannot implement a custom sorter as the following exception results:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'unsupported NSSortDescriptor selector: compareCustom:'

For reference, the sortDate is implemented by :

request.sortDescriptors = @[sortStates]
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                    managedObjectContext:context
                                                                    sectionNameKeyPath:@"states"
                                                                               cacheName:nil];

So it looks like I cannot implement a compareCustom which returns a NSComparisonResult? I feel like it should be possible.. but I am unable to figure out how. If someone can provide some advice I would really appreciate it!

Also for reference, here is my "compareCustom" method

-(NSComparisonResult)compareCustom:(NSString *)anotherState {
if ([(NSString *)self isEqualToString:@"Push"] && [anotherState isEqualToString:@"Canceled"]) {
    return NSOrderedDescending;
}

else if ([(NSString *)self isEqualToString:@"Push"] && [anotherState isEqualToString:@"Busy"]) {
    return NSOrderedDescending;
}

else if ([(NSString *)self isEqualToString:@"Push"] && [anotherState isEqualToString:@"Finished"]) {
    return NSOrderedDescending;
}

else if ([(NSString *)self isEqualToString:@"Push"] && [anotherState isEqualToString:@"Push"]) {
    return NSOrderedSame;
}

else if ([(NSString *)self isEqualToString:@"Finished"] && [anotherState isEqualToString:@"Canceled"]) {
    return NSOrderedDescending;
}

else if ([(NSString *)self isEqualToString:@"Finished"] && [anotherState isEqualToString:@"Busy"]) {
    return NSOrderedAscending;
}
else if ([(NSString *)self isEqualToString:@"Finished"] && [anotherState isEqualToString:@"Finished"]) {
    return NSOrderedSame;
}
else if ([(NSString *)self isEqualToString:@"Finished"] && [anotherState isEqualToString:@"Push"]) {
    return NSOrderedAscending;
}

else if ([(NSString *)self isEqualToString:@"Busy"] && [anotherState isEqualToString:@"Finished"]) {
    return NSOrderedDescending;
}
else if ([(NSString *)self isEqualToString:@"Busy"] && [anotherState isEqualToString:@"Canceled"]) {
    return NSOrderedDescending;
}

else if ([(NSString *)self isEqualToString:@"Busy"] && [anotherState isEqualToString:@"Busy"]) {
    return NSOrderedSame;
}

else if ([(NSString *)self isEqualToString:@"Busy"] && [anotherState isEqualToString:@"Push"]) {
    return NSOrderedAscending;
}

else {
    return NSOrderedSame;
}
}

Upvotes: 7

Views: 4950

Answers (2)

Michał Ciuba
Michał Ciuba

Reputation: 7944

According to the documentation custom sort doesn't work with SQLite persistent store:

The SQL store, on the other hand, compiles the predicate and sort descriptors to SQL and evaluates the result in the database itself. This is done primarily for performance, but it means that evaluation happens in a non-Cocoa environment, and so sort descriptors (or predicates) that rely on Cocoa cannot work. The supported sort selectors are compare: and caseInsensitiveCompare:, localizedCompare:, localizedCaseInsensitiveCompare:, and localizedStandardCompare: (the latter is Finder-like sorting, and what most people should use most of the time). In addition you cannot sort on transient properties using the SQLite store.

So it seems you can't simply force NSFetchedResultsController to display sections in other than alphabetical order.

But you can use a workaround. Add additional, persistent attribute of integer type to your entity (let's call it sectionOrder). Set its value according to the states property (so it will be 0 for "Push", 1 for "Busy" etc.) You can do that in awakeFromInsert method, for example.

Then use @"sectionOrder" as both sectionNameKeyPath and the keypath in sort descriptor. You can set section titles to be "Push", "Busy" etc. using this UITableViewDataSource method:

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.controller sections] objectAtIndex:section];
    return [sectionInfo.objects.firstObject name];
}

Credits to this excellent answer.

Upvotes: 8

Ian MacDonald
Ian MacDonald

Reputation: 14010

The NSSortDescriptor constructor that you're using attempts to call the specified selector on the objects held at the property named @"states". I'm not sure what this value is, but it looks like they might be NSSortDescriptor objects based on the error you're seeing.

Instead of trying to find a selector on an object which may or may not exist, use the block constructor:

You should also really consider rewriting your comparison with integers or something to make it smaller:

static NSArray *stateOrder = @[@"Push", @"Busy", @"Finished", @"Cancelled"];

NSSortDescriptor *sortStates = [NSSortDescriptor sortDescriptorWithKey:@"states"
                                                    ascending:NO
                                                   comparator:^(id obj1, id obj2) {
  NSInteger state1 = [stateOrder indexOfObject:obj1];
  NSInteger state2 = [stateOrder indexOfObject:obj2];
  if (state1 < state2) {
    return (NSComparisonResult)NSOrderedAscending;
  } else if (state1 > state2) {
    return (NSComparisonResult)NSOrderedDescending;
  }
  return (NSComparisonResult)NSOrderedSame;
}];

Upvotes: 5

Related Questions