35

How does one add an object to a relationship property in an NSManagedObject subclass in Swift?

In Objective-C, when you generate an NSManagedObject subclass in Xcode from the data model, there's an automatically generated class extension which contains declarations like:

@interface MyManagedObject (CoreDataGeneratedAccessors)

     - (void)addMySubObject: (MyRelationshipObject *)value;
     - (void)addMySubObjects: (NSSet *)values;

@end

However Xcode currently lacks this class generation capability for Swift classes.

If I try and call equivalent methods directly on the Swift object:

myObject.addSubObject(subObject)

...I get a compiler error on the method call, because these generated accessors are not visible.

I've declared the relationship property as @NSManaged, as described in the documentation.

Or do I have to revert to Objective-C objects for data models with relationships?

Andrew Ebling
  • 10,175
  • 10
  • 58
  • 75

8 Answers8

50

As of Xcode 7 and Swift 2.0 (see release note #17583057), you are able to just add the following definitions to the generated extension file:

extension PersonModel {
    // This is what got generated by core data
    @NSManaged var name: String?
    @NSManaged var hairColor: NSNumber?
    @NSManaged var parents: NSSet?

    // This is what I manually added
    @NSManaged func addParentsObject(value: ParentModel)
    @NSManaged func removeParentsObject(value: ParentModel)
    @NSManaged func addParents(value: Set<ParentModel>)
    @NSManaged func removeParents(value: Set<ParentModel>)
}

This works because

The NSManaged attribute can be used with methods as well as properties, for access to Core Data’s automatically generated Key-Value-Coding-compliant to-many accessors.

Adding this definition will allow you to add items to your collections. Not sure why these aren't just generated automatically...

Kamchatka
  • 3,597
  • 4
  • 38
  • 69
lehn0058
  • 19,977
  • 15
  • 69
  • 109
  • 2
    This should probably be the accepted answer, since it relies on CoreData accessors. – Arnaud Nov 08 '15 at 09:15
  • 6
    Make sure the method name matches the key exactly!! If I have a set of monkeys, the method will be @NSManaged func addMonkeysObject(value:Monkey) - if you leave out the trailing s, it will crash. – DogCoffee Nov 23 '15 at 02:39
  • 1
    What about NSOrderedSet? I get [NSSet intersectsSet:]: set argument is not an NSSet error – Vitalii Vashchenko Dec 25 '15 at 14:56
  • You can't set it to any type you want. Only NSSet/Set will work, because that is what is generated behind the scenes. – lehn0058 Dec 28 '15 at 14:53
  • 1
    @Nycen could you check my answer? I've just found out that it's possible and I think it's even easier. – FranMowinckel Jan 30 '16 at 16:42
  • Is this an oversight on Apples part? These definitions are added automatically in the Objective-C counterpart. – JRam13 Apr 27 '16 at 19:10
  • Ordered to-many relations generate NSOrderedSet. At least in Obj-C that's what you get, and that's the actual class of the relation collections. NSOrderedSet is a little weird class, not inheriting from either NSArray or NSSet - and also with lousy KVC/KVO support (many of the @ operators in KVC do not work) but it does expose all the needed stuff to maintain order in the to-many relations. I've been using it for many years now. – Motti Shneor Jan 28 '18 at 05:48
34

Yeah that's not going to work anymore, Swift cannot generate accessors at runtime in this way, it would break the type system.

What you have to do is use the key paths:

var manyRelation = myObject.valueForKeyPath("subObjects") as NSMutableSet
manyRelation.addObject(subObject)
/* (Not tested) */
Paul Bonneville
  • 1,905
  • 2
  • 13
  • 17
iluvcapra
  • 9,436
  • 2
  • 30
  • 32
  • 3
    The relation is most likely going to come back as a `NSMutableSet` not a `NSMutableArray`. Only an ordered relationship will come back as a `NSMutableArray`. – Marcus S. Zarra Jun 10 '14 at 17:44
  • I shall edit. In this case it'll come back as a set, but only because it's a CoreData unordered relation. To-Many KVO relations (as opposed to merely CD NSManagedObject relations) are almost always arrays otherwise. – iluvcapra Jun 10 '14 at 18:18
  • 7
    And actually you could just use `myObject.mutableSetValueForKey()` and then you don't need a cast. – iluvcapra Nov 03 '14 at 17:26
  • 1
    What about for to-one relationships? If I use `myObject.relationship = otherObject`, will the inverse relationship be set for me? – AnthonyMDev Mar 27 '15 at 00:01
20

Core Data in Objective C automatically creates setter methods (1):

By default, Core Data dynamically creates efficient public and primitive get and set accessor methods for modeled properties (attributes and relationships) of managed object classes. This includes the key-value coding mutable proxy methods such as addObject: and removes:, as detailed in the documentation for mutableSetValueForKey:—managed objects are effectively mutable proxies for all their to-many relationships.

As things currently stand with Swift in Xcode6-Beta2, you'd have to implement those accessors yourself. For example if you have an unordered to-many relationship, from Way to Node, you'd implement addNodesObject like this:

class Way : NSManagedObject {
    @NSManaged var nodes : NSSet

    func addNodesObject(value: Node) {
        self.mutableSetValueForKey("nodes").addObject(value)
    }
}

Key here is that you'd have to use mutableSetValueForKey / mutableOrderedSetValueForKey / mutableArrayValueForKey. On these sets / arrays, you can call addObject and they'll be stored on the next flush.

Bouke
  • 11,768
  • 7
  • 68
  • 102
8

You can just use a typed Set instead which is far easier. Following the example provided by @Nycen and @lehn0058 in the previous answer, you can just write:

extension PersonModel {
    @NSManaged var parents: Set<ParentModel>?
}

And then use the insert and remove methods of the Set.

FranMowinckel
  • 4,233
  • 1
  • 30
  • 26
7

As of Xcode 8 and Swift 3.0, Xcode now generates accessors for relationships. For example, I have an NSManagedObject class Store, that has a one to many relationship with Items; I've called that relationship SellsItems. The generated class for Store now has the following extension to add and remove from SellsItems. Adding or removing items to the relationship is as simple as calling these functions.

// MARK: Generated accessors for sellsItems
extension Store {
    @objc(addSellsItemsObject:)
    @NSManaged public func addToSellsItems(_ value: Item)

    @objc(removeSellsItemsObject:)
    @NSManaged public func removeFromSellsItems(_ value: Item)

    @objc(addSellsItems:)
    @NSManaged public func addToSellsItems(_ values: NSSet)

    @objc(removeSellsItems:)
    @NSManaged public func removeFromSellsItems(_ values: NSSet)
}
Mario Hendricks
  • 727
  • 8
  • 7
6

Expanding on the solution above one to many relationships are NSMutableSet so this allows you to directly add or remove the Person NSManagedObject to the Roles in this case a Person has one Role and Roles have many Person(s)

I have tested this solution under Xcode Beta-3 and this works!

This code takes out the Department to simplify showing the one to one and one to many code required to access Roles from a Person and Persons from a Role.

import CoreData


@objc(Person) class Person: NSManagedObject {

    @NSManaged var name: String

    //One to One relationship in your Model
    @NSManaged var roles: Roles

}


@objc(Roles) class Roles: NSManagedObject {

    @NSManaged var role: String

    //One to Many relationship in your Model
    @NSManaged var persons: NSMutableSet

}

extension Roles {

    func addPersonsObject(value: Person) {
        self.persons.addObject(value)
    }

    func removePersonsObject(value: Person) {
        self.persons.removeObject(value)
    }

    func addPersons(values: [Person]) {
        self.persons.addObjectsFromArray(values)
    }

    func removePersons(values: [Person]) {
        for person in values as [Person] {
            self.removePersonsObject(person)
        }
    }

}
Dean
  • 939
  • 10
  • 30
BadDogApps
  • 61
  • 2
  • 2
    Avoiding use of the plural 'Roles', rather than 'Role' or even the original 'Department', would be better. – GoZoner Nov 14 '14 at 20:24
  • 2
    I used this approach until today where I ran into an issue where a set wouldn't contain an object added to it after saving and loading the context. Using `mutableSetValueForKey` as mentioned in above answer solved this. – csch Apr 20 '15 at 08:57
4

As you only need to set one side of a relationship for both to be set nowadays, it's particularly simple if you have a 1<->many relationship, e.g. a Department object has multiple Person objects, then you can just use:

aPerson.department = aDepartment

If you check you'll find that aDepartment.people (assuming that is the reciprocal relationship you've set up) will now contain the 'aPerson' Person object.

If the relationship is many<->many then one of the more complex solutions above appears necessary.

Peter
  • 1,022
  • 9
  • 12
  • 1
    This answer is a work-around, yet clear and simple solution to most scenarios. However - if the 1-to-many relation is Ordered, and you want to control the order - you will be forced to manipulate via the "many" side, along the lines of other answers (more complex). – Motti Shneor Jan 28 '18 at 05:14
0

Let's say you have the following entities:

  • Person
  • Role
  • Department

In your Person entity, they have a to-many relationship with Role and to-one with Department. Your managed object might look something like this:

class Person : NSManagedObject
{
    @NSManaged var roles : Array<Role>
    @NSManaged var department : Department
}

Relationships with inverses (all should have them) only require one side to be set for the link to be established.

For example, if you set a Person's department property to a Department object, the inverse Department.people property would now also have this Person object contained inside.

Erik
  • 12,730
  • 5
  • 36
  • 42