Custom sorting with NSFetchedResultController (subclassing NSSortDescriptor)
Asked Answered
L

1

7

I want to provide custom sorting using NSFetchedResultsController and NSSortDescriptor.

As custom sorting via NSSortDescriptor message -(id)initWithKey:ascending:selector: is not possible (see here), I tried to use a NSSortDescriptor derived class in order to override the compareObject:toObject: message.

My problem is that the compareObject:toObject: is not always called. It seems that it is called only when the data are already in memory. There is an optimization of some sort that use a database based sort instead of the compareObject:toObject when the data are retrieved from the store the first time. (see here).

My question is : how to force NSFetchedResultscontroller to use the compareObject:toObject: message to sort the data ? (and will it work with large data set)

One solution is to use a binary store instead of a sqlite store but I don't want to do that.

Another solution is:
-call performFetch to sort data via SQL (compareObject not called)
-make a modification to the data and reverse it.
-call performFetch again (compareObject is called)
It does work in my case but it's a hack and I am not sure it will always work (especially with large data set (greater than the batch size)).

UPDATED:You can reproduce with the CoreDataBooks sample.
In RootViewController.m, add this ugly hack:

- (void)viewWillAppear:(BOOL)animated {
    Book* book = (Book *)[NSEntityDescription insertNewObjectForEntityForName:@"Book" 
                 inManagedObjectContext:[self fetchedResultsController].managedObjectContext];
    [[self fetchedResultsController] performFetch:nil];
    [[self fetchedResultsController].managedObjectContext deleteObject:book];
    [self.tableView reloadData];
}

In RootViewController.m, replace the sort descriptor code with:

MySortDescriptor *myDescriptor = [[MySortDescriptor alloc] init];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:myDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];  

Add MySortDescriptor class:

@implementation MySortDescriptor

-(id)init
{
    if (self = [super initWithKey:@"title" ascending:YES selector:@selector(compare:)])
    {

    }
    return self;
}

- (NSComparisonResult)compareObject:(id)object1 toObject:(id)object2
{
    //set a breakpoint here
    return [[object1 valueForKey:@"author" ] localizedCaseInsensitiveCompare:[object2 valueForKey:@"author" ] ];
}

//various overrides inspired by [this blog post][3]
- (id)copy
{
    return [self copyWithZone:nil ];
}
- (id)mutableCopy
{
    return [self copyWithZone:nil ];
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
    return [self copyWithZone:zone ];
}
- (id)copyWithZone:(NSZone*)zone
{
    return [[MySortDescriptor alloc] initWithKey:[self key] ascending:[self ascending] selector:[self selector]];
}
- (id)reversedSortDescriptor
{
    return [[[MySortDescriptor alloc] initWithKey:[self key] ascending:![self ascending] selector:[self selector]] autorelease];
}
@end
Leandro answered 5/11, 2010 at 13:21 Comment(5)
How are you trying to sort your data that NSSortDescriptor, or some combination of multiple NSSortDescriptors is insufficient?Northumbria
I am trying to sort points according to their distance from a reference point. I was hoping to encapsulate the reference point as an ivar in a NSSortDescriptor derived object.Leandro
Hmm, that sounds like an interesting problem. It certainly sounds like you'll have to pull the objects in memory to sort them. You could have a property "referencePoint" on your target entity, and a transient property called distance. Implement distance to calculate the distance between referencePoint and the target entity, sort on distance. Although I like your subclass idea better.Northumbria
It's certainly a safer solution.Leandro
shouldn't that be allocWithZone: instead of alloc in your copyWithZone:?Cut
T
7

In reference to your question and the comments. You are going to need to pull the objects into memory to sort them. Once they are in memory you can use a convenience method to determine distance from a point.

To decrease the number of objects you pull into memory you could calculate max and min values and then filter on those, reducing the radius of your search before you sort.

It is not possible to sort on a calculated value unless it is in memory.

Thao answered 9/11, 2010 at 21:3 Comment(7)
So it means I cannot have the benefits of using the combo UITableViewController+NSFetchedResultsController if I want to provide a custom sort algorithm.Leandro
you cannot just use a NSFetchedResultsController if you are trying to sort on transient data of any form. It is not the algorithm that is the issue but the data. You can still watch the context with a NSFetchedResultsController or some other form of watcher (like ZSContextWatcher) but you will need to do work after the objects are loaded into memory.Thao
Well I can do what I want if I use a binary store instead of the SQLite store. It's too bad, Apple does not provide a way to do custom sorting for the dev who understand the tradeoff in terms of performance.Leandro
It is not an Apple limitation. A binary can sort on transient values because the entire data structure is loaded into memory. When you are fetching from binary you are fetching from memory. When you are fetching from SQLite you are fetching from disk. It is not a performance issue bit a barrier between SQLite and Core Data.Thao
Well, I understand that for the fetching, Core Data is relying on the SQLite based sort. But in case of a dumb binary store it has to do the sort itself after retrieving the data. I was just searching a way to tell CoreData : "Please retrieve the object I need using a SQLite based filtering and then use the same method as you use with binary store to sort the data". If you say that it needs to fetch the data in memory to do that I believe you and I'm ok with that. But it's not exactly my problem.Leandro
That is exactly your problem. For what you are trying to do, an NSFetchedResultsController is insufficient. It is a general purpose tool that is not going to work for your specific needs. You are most likely better off doing a normal NSFetchRequest as I described above, then sorting once you have a filtered set in memory. From there you can set up a watcher against the context for any changes to the data and react accordingly.Thao
Ok... ok... Thank you for your time. I'm not totally convinced but I can't give you half a check.Leandro

© 2022 - 2024 — McMap. All rights reserved.