Can I encode a subclass of NSManagedObject?
Asked Answered
D

3

18

This is for an iPhone App, but I don't think that really matters. I need to send a custom object (which is managed by Core Data) over bluetooth using the iPhone's GameKit. Normally, I would just use an NSKeyedArchiver to package up the object as a data object and ship it over the line, then unarchive the object and I'm done. Of course, I would need to implement the initWithCoder: and encodeWithCoder: methods in my custom object as well.

I'm not sure if this can be done with an NSManagedObject class, which is managed by Core Data or not. Will they play nice together? I'm guessing once I ship the encoded managed object over to the other device and unencode it, I would just add this received object to the other device's context. Is this correct? Am I missing any steps?

Disconcert answered 3/9, 2009 at 6:46 Comment(0)
L
28

An NSManagedObject instance can't meaningfully exist outside of an NSManagedObjectContext instance, so I wouldn't bother trying to do the NSCoding dances required to directly serialize and deserialize an NSManagedObject between two contexts (you can do this; see below). Instead I would create a dictionary with the appropriate attribute key/values (you can get the attribute names via the managed object instance's attribute names via instance.entity.attributesByName.allKeys (you can use [instance dictionaryWithValuesForKeys:keys] to get the dictionary of attribute:value pairs) . I would send relationship information as NSURL-encoded NSManagedObjectIDs. Don't forget to include the instance managedObjectID (as an NSURL) in the dictionary so that you can reconnect any relationships to the object on the other end. You'll have to recursively create these dictionaries for any targets of relationships for the instance you're encoding.

Then send the dict(s) across the wire and reconstitute them on the other end as instances in a new managed object context (you can use setValuesForKeysWithDictionary:).

You may notice that this is exactly what the NSCoder system would do for you, except you would have to use the classForCoder, replacementObjectForCoder: and awakeAfterUsingCoder: along with a custom NSDictionary subclass to handle all the NSManageObject-to-NSDictionary mapping and visa versa. This code is more trouble than it's worth, in my experience, unless you have a complex/deep object graph that you're trying to serialize. For a single NSManagedObject instance with no relationships, it's definitely easier to just do the conversion to a dict and back yourself.

Lateshalatest answered 3/9, 2009 at 18:26 Comment(3)
I had this same problem, and I ended up doing exactly what Barry suggests - it worked fine.Albumose
would it be fine also instead of NSDictionary, I will just have an identical model with that of my NSManagedObject? and there I will do the mapping?Estienne
Could anybody please explain how NSURL-encoded NSManagedObjectIDs is helpful for relationships? Thanks!Compressibility
R
3

This sounds like a job for TPAutoArchiver.

Rosenda answered 3/9, 2009 at 8:2 Comment(0)
P
2

I suggest the dictionary solution for simpler options. However, here is how I solved the issue. My model was already sizable and robust, with custom classes and a single root class above NSManagedObject.

All that I needed was for that single class to call the appropriate designated initializer of NSManagedObject: [super initWithEntity:insertIntoManagedObjectContext:]. This method, and the metadata in an NSEntityDescription is what sets up the implementations of all the dynamic accessors.

- (id)initWithCoder:(NSCoder *)aDecoder {
  CoreDataStack *cds = [LibraryDiscoverer unarchivingCoreDataStack];
  NSEntityDescription *entity = [cds entityDescriptionForName:[[self class] entityName]];
  NSManagedObjectContext *moc = [cds managedObjectContext];
  self = [super initWithEntity:entity insertIntoManagedObjectContext:moc];
  self.lastEditDate = [aDecoder decodeObjectForKey:@"lastEditDate"];
  return self;
}

The CoreDataStack is my abstraction around CoreData. The LibraryDiscoverer is a global access hook to get hold of the core data information. The entityName is a method defined to provide the entity name from the class name; if you follow a naming convention (i.e. class name = entity name) it can be implemented generically.

All the other initWithCoder: methods in my class hierarchy are standard NSCoder, with the note that you don't need to encode both directions of a relationship, CoreData reconnects that for you. (As it always does, including with the dictionary solution.)

Plataea answered 7/7, 2011 at 3:44 Comment(2)
Nice solution. How will you manage this when you have two stacks? I'm currently working on a application that communicates to another instance of the application over the network. In my test target i have a unit test which tests the communication. In this situation, i need two different stacks. How would you solve this in combination with the example you gave above?Garate
i actually create a new, unique stack to do the unarchiving, then save the context (the main stack listens to that and updates), then delete the importing stack. - - - you could keep a mapping of NSCoder -> stack if you wanted to get complex unarchivingCoreDataStackForCoder:(NSCoder *)decoderPlataea

© 2022 - 2024 — McMap. All rights reserved.