Search code examples
iphoneobjective-ccore-datansfetchrequestnssortdescriptor

Sorting an NSFetchRequest by a dependent property using a to-many relationship


Would appreciate some guidance as I'm a bit out of my depth on a Core data issue. I would like to create an NSFetchRequest with an NSSortDescriptor that uses a dependent property based on a to-many relationship.

For Apple's part, the documentation says you cannot do this. What confuses me is that it's only when I use it as part of the NSSortDescriptor that it doesn't work. In the NSPredicate of the request and in my ManagedObject subclass it works.

In short, I have a simple object model that comprises a Venue object with can have many Checkin objects. Each Checkin object has a property called hereNow.

@interface Venue :  NSManagedObject <MKAnnotation>  {}

@property (nonatomic, readonly, retain) NSNumber * hereNow;

@end

@interface Venue (CoreDataGeneratedAccessors)
- (void)addCheckinsObject:(NSManagedObject *)value;
- (void)removeCheckinsObject:(NSManagedObject *)value;
- (void)addCheckins:(NSSet *)value;
- (void)removeCheckins:(NSSet *)value;

@end

@implementation Venue 

- (NSNumber *)hereNow {

return [self valueForKeyPath:@"[email protected]"];

}

@interface Checkin :  Update  {}

@property (nonatomic, retain) Venue * venue;
@property (nonatomic, retain) NSNumber * hereNow;

@end

@implementation Checkin 

@dynamic venue;
@dynamic hereNow;

@end

So far so good. Here's the fetchRequest from a TableView controller

// Create and configure a fetch request with the Venue entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Venue" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];

// Create a predicate to filter the results
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"[email protected] > 0"];
[fetchRequest setPredicate:predicate];

// Create the sort descriptors array.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"[email protected]" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];

// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
                                                                                            managedObjectContext:managedObjectContext 
                                                                                              sectionNameKeyPath:nil
                                                                                                       cacheName:@"Venues"];

With that code as it is I get the following error

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Keypath containing KVC aggregate where there shouldn't be one; failed to handle [email protected]'

But I have a similar fetchRequest for a MapView controller which works perfectly (There I just set the NSSortDecriptor to use the name field). If I do the same for the TableView fetchRequest, that works too and the Venue counts appear to update correctly when new checkin objects are added. So it seems to have no problem using that keyPath in the predicate, just the sortDecriptor.

Apple's documentation however, says this;-

You cannot set up dependencies on to-many relationships. For example, suppose you have an Order object with a to-many relationship (orderItems) to a collection of OrderItem objects, and OrderItem objects have a price attribute. You might want the Order object have a totalPrice attribute that is dependent upon the prices of all the OrderItem objects in the relationship. You can not do this by implementing keyPathsForValuesAffectingValueForKey: and returning orderItems.price as the key-path for totalPrice. You must observe the price attribute of each of the OrderItem objects in the orderItems collection and respond to changes in their values by updating totalPrice yourself.

Which seems clear enough but then I don't understand why I can do the same thing in the predicate? This is my first Core data app so I'm a bit new to it. I've read several questions on KVO, KVC etc. which I haven't really gotten to grips with properly. As you can see, all I'm really trying to do is sort a collection of Venues based on their derived total "hereNow" count. Perhaps I'm going about this in completely the wrong way? If so, would appreciate a friendly steer in the right direction!


Solution

  • A few things that I can think of:

    1. On the SortDescriptor, you can't use @sum to calculate the sort value. You have to calculate that elsewhere. I believe that's why it says "containing KVC aggregate." Because you are using a "key" value to create the sort descriptor, it has to be an actual property on the object.

    2. When you have a derived property in your Venue object, you shouldn't make it dependent upon other objects that traverse across relationships. Your "hereNow" property can't traverse to the Checkin objects to get values to be returned on the Venue object. I believe that derived properties are only permitted to access other properties in the same object.

    One possible thing to try is to change your sort descriptor key to just try initWithKey:@"hereNow".

    The reason it works with your MapView controller (I'm inferring from your question) is that the name field is on the same object and doesn't traverse a relationship to get that value.

    Best thing I can suggest is to add an actual property on your Venue object that gets updated every time you change the Checkins. I know that's not exactly what you want, but it would work.

    By the way, keyPathsForValuesAffectingValueForKey would work on Mac, but not iOS. It would tell the parent object when it needs to be updated. See Registering Dependent Keys section of: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVODependentKeys.html#//apple_ref/doc/uid/20002179-BAJEAIEE

    If you are new to CoreData, I would recommend a great book by Marcus Zarra: https://pragprog.com/titles/mzcd/core-data

    Hope this helps.