IOS5_APPs

NSInvalidArgumentException with NSOrderedSet using CoreData

on June 19, 2012 by Mobile Application Development, Programming, Technology with 4 comments

If you have ever worked on an iOS 5 application and came across a strange issue adding objects to a NSOrderedSet that is part of a NSManagedObject, you are not alone. This bug was reported on Open Radar on September 13, 2011, and was still reproducible in the latest versions of iOS5.

Setting up the Problem

To replicate the problem, create two NSManaged objects.

  1. Create a NSManagedObject called “Student”. Add the attribute “name” of type string.
  2. Create a NSManagedObject called “Lecture”. Add the attribute “name” of type string.
  3. On your Student object, add the relationship “lectures”, and set the Destination to “Lecture”. Make sure the boxes are checked next to “To-Many relationship” and “Ordered”.
  4. On your Lecture object, add the relationship “students”, and set the type to “Student”. Set the Inverse to “lectures”. Make sure the boxes are checked next to “To-Many relationship” and “Ordered”.

A student can have many lectures, and a lecture may have many students. We set the inverse relationship because we might need to know which students are in a particular lecture, as well as which lectures a particular student is enrolled in.

Replicating the Problem

To replicate this problem, create a Lecture and save it to CoreData. Next, create a student, and try to insert it into the “students” relationship in the lecture object you just created:

AppDelegate *delegate = [[UIApplication sharedApplication] delegate];

// Create the lecture
Lecture *lecture = [NSEntityDescription insertNewObjectForEntityForName:@"Lecture" inManagedObjectContext:delegate.managedObjectContext];
lecture.name = @"Obedience Training";

// Create the student
Student *student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:delegate.managedObjectContext];
student.name = @"Rogan";

[lecture addStudentsObject:student];
[delegate saveContext];

What happens when you run this?

When you run this code, assuming you set the inverse relationship, you should get this exception:

‘NSInvalidArgumentException’, reason: ‘*** -[NSSet intersectsSet:]: set argument is not an NSSet’

How can I fix this?

One clean approach to fixing this issue is to add a category to your lectures model:

Note: one reason you should use a category – if you decide to make changes to your CoreData model and regenerate the class, it will not delete your category. If you placed this function in your model, re-generating the model will erase your code.

  1. Select New>File.
  2. In the left pane, select “Cocoa Touch”, and then select “Objective-C category”.
  3. In the “Category on” box, select “Lecture”, and name the Category “methods” (it can be named whatever you like, but I am sticking with “methods” for this small example).

You will notice in your project explorer, a file will be created called “Lecture+methods.h”. This file will contain all of your extra methods for your object.

In your Lecture+methods.h file, override the addStudentsObject: method –

-(void)addStudentsObject:(Student *)value
{
     // Create a mutable set with the existing objects, add the new object, and set the relationship equal to this new mutable ordered set
     NSMutableOrderedSet *students = [[NSMutableOrderedSet alloc] initWithOrderedSet:self.students];
     [students addObject:value];
     self.students = students;
}

That’s it! Running the code should now work.

The following two tabs change content below.

Jonathan Nalewajek

Jon is a senior mobile developer at Cypress North. He's also the co-founder and lead developer of Academy Geeks, where he develops applications for the Android operating system for use amongst academics. Jon has previously held a professor role at SUNY Fredonia teaching mobile development classes. He really likes cold pop.

4 Comments

  • Julian Asamer
    on September 25, 2012 Reply

    Hi, your solution code looks fine except that you forget to actually add the student in the else-case.

    Cheers!

    • Jonathan Nalewajek
      on September 26, 2012 Reply

      Julian,

      Thank you very much for the correction. I was writing this by scratch (instead of copying and pasting from xCode) and missed that line.

      I updated the post to reflect this change.

      Cheers!
      Jon

  • Yuen Boon Yee
    on October 17, 2012 Reply

    I tried numerous variants of this workaround. They appear to work while the application is in-memory. However, after I saved the application context to the Core Data data store, I noticed that the ordered one-to-many relationship isn’t reflected in the .sqlite file. I confirmed this by restoring the object model after re-launching the application.

    In summary, the workaround fixes the object model in memory, but doesn’t save it correctly to the data store. The next time you attempt to re-create the object model, you will find the ordered one-to-many relationships missing.

    • Jonathan Nalewajek
      on October 17, 2012 Reply

      Yuen,

      We had not noticed this issue because we were refreshing our cached data each time the application loaded by making a call to the server. This reloaded all of the entities while the application was in-memory, which, as you stated, worked correctly.

      I just mocked up a demo application to test what you noticed, and saw the problem. I stepped through the debugger to see what was going on, and found a simple fix.

      I noticed that the following if statement always evaluates to true:

      if ([self.students isKindOfClass:[NSMutableOrderedSet class]])

      Because of this, the following line always gets called:
      [(NSMutableOrderedSet *)self.students addObject:value];

      Now, you would think this should work. If self.students is a mutable array, we should simply be able to add a student object to that array, and commit this change to CoreData. However, this is not the case. I have not had time yet to figure out exactly what is going on, but as you noted, the changes are not being committed to CoreData.

      If we comment out everything except what is in the else-statement, things should work appropriately.

      Comment out everything except:

      NSMutableOrderedSet *students = [[NSMutableOrderedSet alloc] initWithOrderedSet:self.attendees];
      [students addObject:value];
      self.students = students;

      I will update the post to reflect this change. Thank you for bringing it to my attention!

      Best,
      Jon

Leave a Reply