21

In my app, I have a NSDictionary whose keys should be instances of a subclass of NSManagedObject.

The problem, however, is that NSManagedObject does not implement the NSCopying protocol which means that no Core Data objects / instances of NSManagedObject can be used as dictionary keys even though the -[hash] method works fine for them.

Was should I do?

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
mrueg
  • 8,185
  • 4
  • 44
  • 66

4 Answers4

35

There are four options:

  1. Use a different object as the dictionary key instead, and lookup from that. [object objectID] or +[NSValue valueWithNonretainedObject:] seem the most obvious
  2. Use CFDictionaryCreateMutable() to create a dictionary with retained keys, rather than copied, instead, and then call CFDictionarySetValue() to store the objects
  3. On OS X or iOS6+, [NSMapTable mapTableWithStrongToStrongObjects] gives you a purely Objective-C equivalent to CFMutableDictionary
  4. Implement NSCopying for your managed object subclass, such that it returns self (with a bumped reference count if you're not using ARC)

Notes

+valueWithNonretainedObject: is pretty dangerous, since it's possible to be left with a dangling pointer; likely best to avoid.

Storing object IDs is fine, apart from the fact that new objects start out life with a temporary ID. That ID then changes to a permanent one when the context is saved to disk (or -obtainPermanentIDsForObjects:… is called). Your mapping code needs to be smart enough to handle this unless it can guarantee that all incoming objects already have a permanent ID.

Implementing NSCopying like this feels a bit icky, but should work just fine. As it happens, this is exactly the approach NSURLSessionTask takes, I presume for dictionary friendliness.

Prior to OS X 10.8 Mountain Lion, it used to be possible to create a regular NSMutableDictionary and then call CFDictionarySetValue() for it. That's no longer the case though; new dictionaries now have proper copy callbacks specified down at the CF level, rather than purely being a feature of NSMutableDictionary.

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
  • I would not consider 1 or 2 an option. I've found that when a managed object ID INSTANCE changes, it's treated as a different key. The managed object IDs must be using the instance address to calculate the unique value derived from the key object. I wondered if 2 would solve the issue by eliminating key copies. No such luck. – stephen May 02 '13 at 20:11
  • @stephen thanks for prompting me to bring the answer up-to-date – Mike Abdullah May 03 '13 at 14:54
  • the depth of your knowledge shows a great deal of time spent in research. Thanks for the update. BTW: the motivation for my comment was a bit of an edge case: I've been pulling hair in responding to NSManagedObjectContextObjectsDidChangeNotification – stephen May 04 '13 at 06:45
  • Why not just implementing NSCopying in the subclass? – cfischer Apr 22 '15 at 21:24
  • It's a fair point. Feels slightly odd to me, but I don't see a major reason why not. I'll add that to my answer – Mike Abdullah Apr 24 '15 at 09:43
1

I suggest to use [[[myManagedObject objectID] URIRepresentation] absoluteString] as your key.

tlindner
  • 382
  • 1
  • 6
  • 4
    There's no point to all this. NSManagedObjectID can be used fine as-is as it implements NSCopying. – Mike Abdullah Sep 30 '09 at 13:43
  • 1
    There is -- you can archive the URIRepresentation of the object ID (via NSCoding). Very useful if you want to copy an NSManagedObject into the pasteboard. – adib Jul 26 '10 at 12:31
  • 1
    Well yes, but the point here is the poster wants to construct a dictionary keyed by managed objects; doesn't sound that's going on the pasteboard to me – Mike Abdullah May 03 '13 at 14:53
  • I think there is more to this. Managed Object IDs can change. They start out as a temporary ID and then may be turned into a permanent ID. If you want to eliminate this case for potential issues using the URI representation is the way to go. – Christian Kienle Mar 13 '21 at 14:19
  • @MikeAbdullah also: NSManagedObjectID references NSEntityDescription (through its entity-property). And the documentation of NSEntityDescription states that NSEntityDescriptions should not be used as a key in dictionaries: Since NSDictionary copies its keys and requires that keys both conform to the NSCopying protocol and have the property that copy returns an object for which [[object copy] isEqual:object] is true, you should not use entities as keys in a dictionary. – Christian Kienle Mar 13 '21 at 14:22
0

I had a similar problem, in which I needed to bundle several entities with additional data for each, and initially tried:

@{entity1:data1, @entity2:data2, @entity3:data3}

this didn't work for the reason above (NSCopying), so I did:

@[
   @{@"entity":entity1, @"data":data1},
   @{@"entity":entity2, @"data":data2},
   @{@"entity":entity3, @"data":data3}
]

But this solution makes sense only if you don't need dictionary style access to these entities or are happy to iterate to find what you need. In my case this was a packaging problem. Note that if you pass these entities around the NSManagedObjectContext need to be the same to use them.

0

Could you create a wrapper class, that contains a reference to the instance of NSManagedObject that you want to use as a dictionary key? You could then make this wrapper class implement NSCopying, along with a hash method (perhaps just calling the NSManagedObject's hash method), and use this wrapper as the dictionary key.

Tom Jefferys
  • 13,090
  • 2
  • 35
  • 36