36

I am having an issue (understanding issue to be honest) with NSFetchedResultsController and the new NSOrderedSet relationships available in iOS 5.

I have the following data-model (ok, my real one is not drawer's and sock's!) but this serves as a simple example:

enter image description here

Drawer and Sock are both NSManagedObjects in a Core Data model/store. On Drawer the socks relationship is a ordered to-many relationship to Sock. The idea being that the socks are in the drawer in a specific order. On Sock the drawer relationship is the inverse of the socks relationship.

In a UIViewController I am drawing a UITableView based on these entities. I am feeding the table using a NSFetchedResultsController.

- (NSFetchedResultsController *)fetchedResultsController1 {
    if (_fetchedResultsController1 != nil) {
        return _fetchedResultsController1;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Sock" inManagedObjectContext:[NSManagedObjectContext MR_defaultContext]];
    [fetchRequest setEntity:entity];

    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"drawer.socks" ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];

    self.fetchedResultsController1 = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext MR_defaultContext] sectionNameKeyPath:nil cacheName:@"SocksCache"];
    self.fetchedResultsController1.delegate = self;

    return _fetchedResultsController1;    
}

When I run this, I get the following errror: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'to-many key not allowed here'

This makes sense to me, as the relationship is a NSOrderedSet and not a single entity to compare against for sorting purposes.

What I want to achieve is for the Socks to appear in the UITableView in the order specified in the socks relationship. I don't really want to have a sort order but NSFetchedResultsController, which is a great component is insisting there has to be one. How can I tell it to use the socks order on the Drawer entity. I don't want the table to show Drawer entities at all.

Note: I am using this within an iOS5 application only, so ordered relationships are available.

Anyone that can offer me any direction, would be greatly appreciated. Thanks for your time.

Edit: So the tableview that display's socks does so for just one drawer. I just want the table view to honor the order that the socks relationship contains. I'm not sure what to set the sort criteria to make sure that happens.

Damien
  • 2,421
  • 22
  • 38
  • Damien, you seem to have resolved this. When you say "in the order specified in the `socks` relationship" - how do you go about specifying the order of the relationship? In the editor I can just see a check-box (ordered or not ordered), but nothing to control the sort criteria. – jhabbott Jun 28 '12 at 23:44
  • 1
    @jhabbott. When you specify an ordered relationship (using checkbox), then the relationship is represented by a NSOrderedSet. You can then use the usual ordered set methods to control the order. – Damien Jul 01 '12 at 09:25
  • Ok, so now I start to use the generated `insertObject:inSocksAtIndex:` method, but I get an unrecognized selector exception. I thought the managed object context would add this at run-time? – jhabbott Jul 02 '12 at 19:04
  • 1
    @jhabbott So does everyone, but it appears to be a long standing bug with Apple. Check out the answer by 'LeeIII' in http://stackoverflow.com/questions/7385439/exception-thrown-in-nsorderedset-generated-accessors You'll have to implement the method like this yourself until the bug is fixed (and its been there for years!) – Damien Jul 02 '12 at 20:21
  • Thanks for the help. I decided not to use the *ordered* flag at all and instead create a separate `NSFetchRequest` to get `socks` with a `[NSPredicate predicateWithFormat:@"drawer == %@", theDrawer]` and my own sort descriptor on the same request. This way I can sort my socks by `size` or `color` in different circumstances and sync them with iCloud. – jhabbott Jul 02 '12 at 21:01
  • 1
    Understandable. Ordered relationships promise and deliver little in Core Data. I just really wish they worked better. – Damien Jul 02 '12 at 21:11
  • @jhabbott I'd appreciate an up vote on the question if you haven't already done so! Thanks! – Damien Jul 02 '12 at 21:12

6 Answers6

5

Damien,

You should just make your NSFetchRequest use the array form of the ordered set. It will operate fine. Your controller needs an attribute to sort on. Hence, you'll need to specify that too.

Andrew

adonoho
  • 4,339
  • 1
  • 18
  • 22
  • Hi Andrew. Thanks very much for taking the time to answer the question. I think I'm understanding what you are saying and I know that I can get a NSArray from an ordered set. I'm just not sure how I feed that into a NSFetchRequest. I'm probably overthinking this. Could you prod me in the correct direction? – Damien Jan 07 '12 at 02:10
  • 7
    Damien, A simple predicate is all you need. For example: `@"self in %@", orderedSet.array`. The sort descriptor should be equally simple -- pick an attribute that does not change the sort order. Andrew – adonoho Jan 07 '12 at 15:15
  • 1
    Hi Andrew, thanks a million for the assistance. That worked. It was the sort order than wouldn't actually changed that was the bit of thinking I was missing. Somethings in an entity that might be hard to find such an attribute, so I'm thinking sometimes I might have to have a dummy attribute that won't change the sort order. It seems like NSFetchedResultsController might need to catch up a bit with ordered relationships – Damien Jan 07 '12 at 18:52
  • adonoho, ordered Relationships are designed for situations where the order is arbitrary, such as arranging one's favorite colors in order. This means that a sort descriptor will likely not exist that sufficiently describes this arbitrary nature. – Scott Ahten Jan 09 '12 at 16:08
  • Scott, I agree with your assessment of the role of an ordered set. The challenge the OP made was that he wanted to use the arbitrary order in which his entities arrived from the server. The `NSFetchedResultsController` requires a sort descriptor. Hence, he needs some kind of serial number to capture this ordering. – adonoho Jan 10 '12 at 14:24
  • Scott, Further,I think he should pick some pre-existing attribute upon which to sort rather than capture an arbitrary order to present to a user. Users tend to not understand why something is arbitrary. IOW, it isn't a good conceptual model. But this is his app and UX. I'm sure he has good reasons to present an arbitrary order. – adonoho Jan 10 '12 at 14:25
  • 1
    Ordered relationships usually represent a user defined preference which isn't normally expressed in a property of the objects themselves. It's unclear why he'd use a ordered relationship if the data already had properties that were suitable for sorting. He could add a displayOrder property specifically for this purpose, but this would require maintaining this value across all objects in the relationship, at which point using an ordered relationship is probably not a good fit. – Scott Ahten Jan 10 '12 at 22:33
  • Independent of when it make sense to use a ordered relationship I can not validate that a NSFetchedResultsController provide the ordering of the ordered relationship. I tried several sort descriptor's but non is working correct. Is somewhere an example available which prove this? – Stephan Jan 26 '12 at 21:16
  • Stephan, An `NSFetchedResultsController` just uses an `NSFetchRequest`. You should focus your testing using that mechanism before you use the added complexity of the controller. Andrew – adonoho Jan 27 '12 at 14:40
  • Ok reading all the comments agin lead to the result: It is NOT possible to fetch the arbitrary ordering of an `NSFetchRequest`. Correct? – Stephan Jan 27 '12 at 19:51
  • Stephan, You are basically looking for the insertion order in the database. Have you actually just tried to see what order the data returns in when you query without a predicate? (BTW, this is a general technique to use with Core Data or any sufficiently complex framework. Just run a little experiment and see what happens. Frankly, that is way faster than asking a question on Stack Overflow.) I predict you will ultimately be dissatisfied by the result. I don't understand your reluctance to adding a field that maintains your insertion order, such as a date stamp, obj.date = NSDate.date. Andrew – adonoho Jan 28 '12 at 14:25
  • 1
    OK I understand how CoreData works and yes a fetch returns the inseration order. I have no problem to use a field, but 1. this was not the topic of this question and second this was the old solution in this case you can use the normal relationship (better for iCloud too). I guess (as myself) most of the people evaluated how they can find a binding to an UITableView. The result is `NSFetchedResultsController` is IMHO not appropriate. Therefore I think currently (iOS5.0) this answer is wrong and the seconde is corrent. Hopefully Apple will improve this in the next version. – Stephan Jan 28 '12 at 15:15
  • The accepted answer is misleading and incorrect. Stephan is correct: `NSFetchedResultsController` is NOT appropriate, one should NOT have to add a dummy property to get the correct ordering. Instead, simply index into the ordered set as Lindemann demonstrates – Ilias Karim Jun 25 '12 at 11:45
  • So how do you tell CoreData how to order the `NSOrderedSet`? For example if I create a fetch request to get a `Drawer` by `drawerName`, then I access `theDrawer.socks` - these socks are ordered, but by what criteria? – jhabbott Jun 28 '12 at 23:40
4

I found this thread while looking for an answer to the exact question posed by the OP.

I never found any examples of presenting such data in a tableView without adding the additional sort field. Of course, adding the sort field pretty much eliminates any benefit in using the ordered relationship. So given that I got this working, I thought it might be helpful to others with the same question if I posted my code here. It turned out to be quite simple (much simpler than using an additional sort field) and with apparently good performance. What some folks may not have realized (that includes me, initially) is that the type (NSOrderedSet) of the "ordered, to-many" relationship attribute has a method for getting objectAtIndex, and that NSMUtableOrderedSet has methods for inserting, and removing objectAtIndex.

I avoided using the NSFetchedResultsController, as some of the posters suggested. I've used no array, no additional attribute for sorting, and no predicate. My code deals with a tableView wherein there is one itinerary entity and many place entities, with itinerary.places being the "ordered, to-many" relationship field. I've enabled editing/reordering, but no cell deletions. The method moveRowAtIndexPath shows how I've updated the database with the reordering, although for good encapsulation, I should probably move the database reordering to my category file for the place managed object. Here's the entire TableViewController.m:

//
//  ItineraryTVC.m
//  Vacations
//
//  Created by Peter Polash on 8/31/12.
//  Copyright (c) 2012 Peter Polash. All rights reserved.
//

#import "ItineraryTVC.h"
#import "AppDelegate.h"
#import "Place+PlaceCat.h"
#import "PhotosInVacationPlaceTVC.h"


@interface ItineraryTVC ()

@end

@implementation ItineraryTVC

#define DBG_ITIN YES

@synthesize itinerary ;

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.navigationItem.rightBarButtonItem = self.editButtonItem ;
}

- (void) viewWillAppear:(BOOL)animated
{

    [super viewWillAppear:animated] ;

    UIManagedDocument *doc = UIAppDelegate.vacationDoc;

    [doc.managedObjectContext performBlock:^
     {   // do this in the context's thread (should be the same as the main thread, but this made it work)

         // get the single itinerary for this document

         self.itinerary = [Itinerary setupItinerary: doc ] ;
         [self.tableView reloadData] ;
     }];

}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown );
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
    return [self.itinerary.places count ];
}


- (UITableViewCell *) tableView: (UITableView *) tableView
          cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static NSString *CellIdentifier = @"Itinerary Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier ];


    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault   reuseIdentifier: CellIdentifier];
    }

    Place *place = [self.itinerary.places objectAtIndex:indexPath.row];

    cell.textLabel.text       = place.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%d photos", [place.photos count]];

    return cell;
}


#pragma mark - Table view delegate


- (BOOL)    tableView: (UITableView *) tableView
canMoveRowAtIndexPath:( NSIndexPath *) indexPath
{
    return YES;
}

-(BOOL)     tableView: (UITableView *) tableView
canEditRowAtIndexPath: (NSIndexPath *) indexPath
{
    return YES ;
}

-(void)  tableView: (UITableView *) tableView
moveRowAtIndexPath: (NSIndexPath *) sourceIndexPath
       toIndexPath: (NSIndexPath *) destinationIndexPath
{
    UIManagedDocument * doc = UIAppDelegate.vacationDoc ;

    [doc.managedObjectContext performBlock:^
    { // perform in the context's thread 

        // itinerary.places is the "ordered, to-many" relationship attribitute pointing to all places in itinerary
        NSMutableOrderedSet * places = [ self.itinerary.places  mutableCopy ] ;
        Place *place                 = [ places objectAtIndex:  sourceIndexPath.row] ;

        [places removeObjectAtIndex: sourceIndexPath.row ] ;
        [places insertObject: place   atIndex: destinationIndexPath.row ] ;

        self.itinerary.places = places ;

        [doc saveToURL: doc.fileURL   forSaveOperation: UIDocumentSaveForOverwriting completionHandler: ^(BOOL success) {
            if ( !success ) NSLog(@"Error saving file after reorder, startPos=%d, endPos=%d", sourceIndexPath.row, destinationIndexPath.row) ;
        }];
    }];

}

- (UITableViewCellEditingStyle) tableView: (UITableView *) tableView
            editingStyleForRowAtIndexPath: (NSIndexPath *) indexPath
{
    return ( UITableViewCellEditingStyleNone ) ;
}

- (void) prepareForSegue:(UIStoryboardSegue *) segue   sender: (id) sender
{
    NSIndexPath *indexPath = [self.tableView    indexPathForCell: sender] ;
    PhotosInVacationPlaceTVC  * photosInVacationPlaceTVC = segue.destinationViewController ;

    Place *place = [self.itinerary.places objectAtIndex:indexPath.row ];

    photosInVacationPlaceTVC.vacationPlace        = place ;
    photosInVacationPlaceTVC.navigationItem.title = place.name ;

    UIBarButtonItem *backButton =
    [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStylePlain target:nil action:nil];
    self.navigationItem.backBarButtonItem = backButton;

}


@end
ronalchn
  • 12,225
  • 10
  • 51
  • 61
petelp
  • 1
  • 2
3

You can give Sock an index attribute and order the socks by them:

sock01.index = [sock01.drawer.socks indexOfObject:sock01];
Lindemann
  • 3,336
  • 3
  • 29
  • 27
  • +1 see also [Core Data ordering with a UITableView and NSOrderedSet](http://stackoverflow.com/questions/9875557/core-data-ordering-with-a-uitableview-and-nsorderedset/11188696#11188696) – Ilias Karim Jun 25 '12 at 11:46
  • 1
    Doesn't that mean fetching all the socks in sock01.drawer, which is exactly the kind of thing you use an NSFetchedResultsController to avoid? – Neil Gall Nov 05 '12 at 11:21
3

To add some more clarity to the answer from adonoho (thanks mate) which helped me work it out - rather than trying to specify the to-many relationship as any sorting key which I also couldn't get working, specify the belongs-to relationship in a predicate to select which entities you want in the fetched results controller, and specify the belongs-to relationship as the sort key.

This satisfies the primary aim of having the results in a NSFetchedResultsController with all that goodness, and respects the order in the to-many relationship.

In this particular example (fetching socks by drawer):

// socks belong-to a single drawer, sort by drawer specified sock order
fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:@"drawer" ascending:YES]];

// drawer has-many socks, select the socks in the given drawer
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"drawer = %@", drawer];

Essentially, this uses a given drawer to specify which socks should go into the NSFetchedResultsController, and the ordering as specified by the to-many relationship.

Looking at the generated SQL (annotated from my example using different entity names) via a core data SQL debug:

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, ...fields..., t0.ZDRAWER, t0.Z_FOK_DRAWER FROM ZSOCKS t0 WHERE  t0.ZDRAWER = ?  ORDER BY  t0.Z_FOK_DRAWER

you can see the SQL ordering using the Z_FOK_DRAWER column, which Core Data uses for the position of that sock.

crafterm
  • 1,831
  • 19
  • 17
  • I was very happy to see that this works!... But then I discovered that it causes errors when using a `NSFetchedResultsController` - changes to relationship membership cause a crash with: ```CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[_NSFaultingMutableSet compare:]: unrecognized selector sent to instance 0x283a2a660 with userInfo (null)``` – Andrew Bennet Feb 16 '19 at 13:06
2

As far as I understand this functionality, it allows to have ordered Socks in every Drawer. As Apple writes in documentation:

You should use them only if a relationship has intrinsic ordering that is critical to its own representation—such as the steps in a recipe.

This means you can't fetch all Socks using sorted relation. Sorted Socks will be available only in every Drawer object.

Tomasz Wojtkowiak
  • 4,910
  • 1
  • 28
  • 35
  • Hi thanks for the reply. Sorry, I wasn't being clear. The table represents a drawer, so I only want the table to display the Socks in that particular drawer. I can do that, but what I don't know is how to set the sort criteria so that it displays the socks in the correct order occording to the parent entity (the drawer's) socks ordered relationship. I'll update my question. Thanks again. – Damien Jan 06 '12 at 16:25
1

As I just answered here, I prefer just adding a new property to my NSManagedObject via a category.

Just add a method:

- (NSUInteger)indexInDrawerSocks
{
    NSUInteger index = [self.drawer.socks indexOfObject:self];
    return index;
}

Then in your NSFetchedResultsController, use sort descriptor:

fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"indexInDrawerSocks" ascending:YES]];
Community
  • 1
  • 1
Richard Venable
  • 8,310
  • 3
  • 49
  • 52
  • 6
    This does not seem to work with SQL backed setups. I tried this and the fetch request throws an exception: `*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'keypath indexInBookChapters not found in entity '` – acoward Nov 03 '12 at 21:49