NSInvalidArgumentException with NSOrderedSet using CoreData

data

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.

4 Comments

  1. Author's Headshot
    Julian Asamer September 25, 2012
    Reply

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

    Cheers!

    • Author's Headshot
      Jonathan Nalewajek September 26, 2012

      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

  2. Author's Headshot
    Yuen Boon Yee 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.

    • Author's Headshot
      Jonathan Nalewajek October 17, 2012

      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

Your email address will not be published. Required fields are marked *

Meet the Author

jnalewajek
Senior Software Engineer

Jonathan Nalewajek

Jonathan is a Senior Software Engineer who joined Cypress North in 2012 and works out of our Buffalo office. He brings more than 13 years of experience to our development team. 

Jon, known to our team and clients as J2, works closely with our clients to develop strategic solutions for their problems that will fit their internal infrastructure. He specializes in full-stack development using C#, PHP, HTML, CSS, and JavaScript. 

The Buffalo native graduated from SUNY Fredonia with a Bachelor of Science in computer science. Jon later returned to Fredonia to teach a mobile software development course. 

When he’s not at work, Jon loves being active and enjoying the outdoors - whether it’s climbing, hiking, camping, or snowboarding. He also likes woodworking and baking different breads and pizzas. Jon and his wife have a dachshund named Charlie, who Jon considers his best friend.