Locations in Core Data sorted by distance via NSFetchedResultsController?
Asked Answered
A

2

8

I have a set of entity objects in my iOS Core Data database that describe something at a location. Let's call the entity Location. I have implemented this by having two attributes on Location that refer to the location - latitude and longitude, both doubles. There are other elements, like name.

I am using a NSFetchedResultsController to bind the entities to a UITableViewController. What I would like to do is have the results sorted by distance to a given CLLocationCoordinate2D. In an really ideal scenario, I'm able to refresh that list to recalculate the sort based on a new location. Therefore, this sort would depend on two keys, and a third "static" variable (one that doesn't vary across the items in the collection).

I think I could figure out how to do this if I was sorting an arbitrary list with NSSortDescriptors. However, I don't control how the sort descriptors are used in an NSFetchedResultsController.

Is there a way that I could configure my entities, my NSFetchedResultsController, my NSSortDescriptors, etc. to accomplish this? I suspect that the answer lies not in creating a fancy NSSortDescriptor but instead in creating a transient attribute in the entity that represents the distance-to-me, and recalculating that attribute periodically. However, I'm new enough to Core Data that I'm not sure how to best do this (iterate over all entities and recalculate a field). I'm also not sure if NSSortDescriptors will work on Transient attributes.

Abba answered 6/12, 2012 at 19:31 Comment(6)
Unfortunately, a fetched results controller cannot sort on transient attributes or calculated properties. See #13293082 or #12028269 for a similar issues and references to the documentation.Amnesty
Thank you! I saw that one, and that's why I thought that the answer might be in calculating values in my Core Data entities. Let's take Transient off the table - if it's not a transient field, how would I go about periodically updating the set of entities?Abba
Do you really need a fetched results controller? If you fetch the objects into an array you can sort that array in memory and use as data source for the table view.Amnesty
The NSFetchedResultsController, from my understanding, represents a very convenient mapping of Core Data notifications to table-appropriate events. I have considered replacing it, as you said, but the purpose of this question is to see if there's another way that would let me keep using NSFetchedResultsController.Abba
That is correct, if you want "live" updates of the table view due to inserted/deleted/modified Core Data objects, then the FRC is very convenient. But then you have to precompute the distance of each object and store it in a (persistent) attribute.Amnesty
Thanks, Martin. I can't figure out how to tag your comment as the "answer", but that's the confirmation I was looking for.Abba
A
14

(From the comments:)

A fetch request for a (SQLite based) Core Data store cannot use sort descriptors based on transient attributes or Objective-C based predicates.

If you don't want to lose the advantages of a fetched results controller (like animated table view updates, automatic grouping into sections etc.) you have to pre-compute the distance to the current location and store it in a (persistent) attribute of your objects.

Alternatively, you could fetch all objects and sort them in memory. In that case you can use arbitrary sort descriptors. But that cannot be combined with a fetched results controller, so you would have to register for changes in the managed object context and reload the table if necessary.

Amnesty answered 7/12, 2012 at 6:22 Comment(0)
I
0

I discovered the BSFetchedResultsController github project that is a NSFetchResultsController subclass that does what Martin suggested in that it sorts in memory using a arbitrary sort descriptor, furthermore it also registers for changes to the context and calculates any index changes again taking into account the arbitrary sort descriptor. All in all a very impressive feat! I successfully used it to sort by distance as follows:

BSFetchedResultsController* fetchedResultsController = [[BSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];

//  create a location to compare distance to, e.g. current location
CLLocation* sourceLocation = [[CLLocation alloc] initWithLatitude:55.87595153937809 longitude:-4.2578177698913855];

// compare the distance from both to the source location
fetchedResultsController.postFetchComparator= ^(id a, id b) {
    Venue* v1 = (Venue*)a;
    Venue* v2 = (Venue*)b;

    double d1 = [v1.coreLocation distanceFromLocation:sourceLocation];
    double d2 = [v2.coreLocation distanceFromLocation:sourceLocation];

    return [@(d1) compare:@(d2)];
};

NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}

Since its an old project it doesn't have ARC, so when you include the two files remember to mark the .m with Compiler Flags -fno-objc-arc in the Target, Build Phases. Also be aware the developer thinks the code is not production ready so be sure to do adequate testing if using it.

In my code above I have a transient property coreLocation on my Venue managed object subclass, you can see how to achieve that here. Also the distance calculation is inefficient, you might want to cache the distance in the object rather than re-calculate it every comparison.

Finally, it appears this project came to be because of the creator Daniel Thorpe's Stackoverflow question going unanswered, causing him to solve the problem and post the only answer himself, so I think if you find his project useful you could kindly up-vote his post as I did.

Impotent answered 23/4, 2015 at 0:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.