Since the NSManagedObject
class doesn’t conform to the NSCopying
protocol, there seems to be no easy way to archive a bunch of interrelated managed objects. Once you archive a part of your object graph, you could use the serialized representation for various purposes – sending it over a network, making backups, etc.
The way I figured this could work is to make my NSManagedObject
subclass somehow convert an instance of itself into an NSDictionary
instance, which would hold all properties and relationships for that particular object. The NSDictionary
class conforms to the NSCopying
protocol, so once you have the dictionary, you can do whatever you like with it. Of course, you would also need an inverse mechanism that converts an NSDictionary
instance into your NSManagedObject
.
In order to make the process as transparent as possible, I figured I should create an NSManagedObject
subclass, which would sit right between NSManagedObject
and your model classes in the inheritance tree. A perfect solution would be able to convert a given object from/to a dictionary without knowing the actual type of the object being converted.
It turns out this solution is possible, and is pretty straightforward to implement. So, let’s call this class ExtendedManagedObject
.
@interface ExtendedManagedObject : NSManagedObject {
BOOL traversed;
}
@property (nonatomic, assign) BOOL traversed;
- (NSDictionary*) toDictionary;
- (void) populateFromDictionary:(NSDictionary*)dict;
+ (ExtendedManagedObject*) createManagedObjectFromDictionary:(NSDictionary*)dict
inContext:(NSManagedObjectContext*)context;
@end
So, the interface is pretty straightforward, we basically have two methods that do all the work for us, toDictionary
and populateFromDictionary:
. The createManagedObjectFromDictionary:inContext:
class method is just a helper for creating new managed objects directly from a dictionary. Let’s see how each of these is implemented.
- (NSDictionary*) toDictionary
{
self.traversed = YES;
NSArray* attributes = [[[self entity] attributesByName] allKeys];
NSArray* relationships = [[[self entity] relationshipsByName] allKeys];
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:
[attributes count] + [relationships count] + 1];
[dict setObject:[[self class] description] forKey:@"class"];
for (NSString* attr in attributes) {
NSObject* value = [self valueForKey:attr];
if (value != nil) {
[dict setObject:value forKey:attr];
}
}
for (NSString* relationship in relationships) {
NSObject* value = [self valueForKey:relationship];
if ([value isKindOfClass:[NSSet class]]) {
// To-many relationship
// The core data set holds a collection of managed objects
NSSet* relatedObjects = (NSSet*) value;
// Our set holds a collection of dictionaries
NSMutableSet* dictSet = [NSMutableSet setWithCapacity:[relatedObjects count]];
for (ExtendedManagedObject* relatedObject in relatedObjects) {
if (!relatedObject.traversed) {
[dictSet addObject:[relatedObject toDictionary]];
}
}
[dict setObject:dictSet forKey:relationship];
}
else if ([value isKindOfClass:[ExtendedManagedObject class]]) {
// To-one relationship
ExtendedManagedObject* relatedObject = (ExtendedManagedObject*) value;
if (!relatedObject.traversed) {
// Call toDictionary on the referenced object and put the result back into our dictionary.
[dict setObject:[relatedObject toDictionary] forKey:relationship];
}
}
}
return dict;
}
First, this function enumerates all attributes and relationships of the object. It then uses key-value coding methods to fetch attribute values and store them into the dictionary object. It also stores its class string into the dictionary, so that the unpacker method knows which object to instantiate.
The procedure is a bit different for relationships, since they can be “to-one” and “to-many”. For “to-many” relationships, we need to fetch a set of all related objects, convert each of them into a dictionary recursively by calling toDictionary
, and then add the newly created dictionary to our object’s own dictionary representation.
For “to-one” relationships the process is simpler – we need to check if the object has already been traversed (to prevent infinite recursion), and if not, we convert it to a dictionary and add it to our object’s dictionary representation.
Note: the traversed
variable is a very primitive way to check for recursion, as it will only work if you convert an object to a dictionary once in its lifetime. Every subsequent call to toDictionary
would only create a shallow dictionary, containing only attributes of the object, since all of its related objects have traversed
set to true. The proper implementation is left as an exercise for the reader. ;)
So, on with the methods for converting a dictionary to a managed object:
- (void) populateFromDictionary:(NSDictionary*)dict
{
NSManagedObjectContext* context = [self managedObjectContext];
for (NSString* key in dict) {
if ([key isEqualToString:@"class"]) {
continue;
}
NSObject* value = [dict objectForKey:key];
if ([value isKindOfClass:[NSDictionary class]]) {
// This is a to-one relationship
ExtendedManagedObject* relatedObject =
[ExtendedManagedObject createManagedObjectFromDictionary:(NSDictionary*)value
inContext:context];
[self setValue:relatedObject forKey:key];
}
else if ([value isKindOfClass:[NSSet class]]) {
// This is a to-many relationship
NSSet* relatedObjectDictionaries = (NSSet*) value;
// Get a proxy set that represents the relationship, and add related objects to it.
// (Note: this is provided by Core Data)
NSMutableSet* relatedObjects = [self mutableSetValueForKey:key];
for (NSDictionary* relatedObjectDict in relatedObjectDictionaries) {
ExtendedManagedObject* relatedObject =
[ExtendedManagedObject createManagedObjectFromDictionary:relatedObjectDict
inContext:context];
[relatedObjects addObject:relatedObject];
}
}
else if (value != nil) {
// This is an attribute
[self setValue:value forKey:key];
}
}
}
+ (ExtendedManagedObject*) createManagedObjectFromDictionary:(NSDictionary*)dict
inContext:(NSManagedObjectContext*)context
{
NSString* class = [dict objectForKey:@"class"];
ExtendedManagedObject* newObject =
(ExtendedManagedObject*)[NSEntityDescription insertNewObjectForEntityForName:class
inManagedObjectContext:context];
[newObject populateFromDictionary:dict];
return newObject;
}
The createManagedObjectFromDictionary:inContext:
method first creates an ExtendedManagedObject instance, using the “class” key to extract class information, and inserts it into the managed object context. It then calls populateFromDictionary:
, which iterates through all keys in the dictionary and checks the type of object returned – it can be a simple attribute, a to-one relationship, or a to-many relationship.
The trick that does most of the dirty Core Data work is a call to [self mutableSetValueForKey:key]
. It returns a proxy set summoned by the Core Data subsystem, which does all the database magic for you when you add objects to it. So when you call addObject:
on a proxy set object, you’re actually establishing a relationship between the object being added to the set and the owner of the proxy set. Cool stuff!
So there you go, all you have to do is make each of your model classes a subclass of ExtendedManagedObject
instead of NSManagedObject
, and you can use toDictionary
, populateFromDictionary:
and createManagedObjectFromDictionary:inContext:
to play around with your managed objects. For example, if you have a Person
entity in your model, your header file for that class might look something like this:
@interface Person : NSManagedObject {
}
@property (nonatomic, retain) NSString* name;
@end
Your Person
class needs to be a subclass of ExtendedManagedObject
, so you need to import the ExtendedManagedObject.h
header file, and change “NSManagedObject” to “ExtendedManagedObject”:
#import "ExtendedManagedObject.h"
@interface Person : ExtendedManagedObject {
}
@property (nonatomic, retain) NSString* name;
@end
Now that your Person
class is a subclass of ExtendedManagedObject
, you can convert a Person
instance to a dictionary with:
Person* person = ... // fetch a person from the core data store, as usual
NSDictionary* personDict = [person toDictionary];
To do the inverse, you need to call the createManagedObjectFromDictionary:inContext
method, but since it’s a class method, you need to invoke it statically:
Person* person = ... // fetch a person from the core data store, as usual
NSDictionary* personDict = [person toDictionary];
NSManagedObjectContext* context = ... // your managed object context reference
Person* anotherPerson = [ExtendedManagedObject createManagedObjectFromDictionary:personDict inContext:context];
The above code would convert person
to a dictionary (personDict
), and then use that dictionary to create a new Person object in the context and assign it to anotherPerson
. If you save the context at this point, your store will contain two identical Person objects.
You can also update an existing managed object using the populateFromDictionary:
method:
NSDictionary* personDict = ... // Your updated person dictionary (fetched from a web service, for example)
NSManagedObject* person = ... // Fetch your Person managed object from a context
// Update the person managed object using data from the dicitonary
[person populateFromDictionary:personDict];
You can check out the entire code from this gist on Github.
In my next post, I’ll discuss a simple way to load your SQLite database with predefined data when your application is first launched. Stay tuned.
UPDATE: PCKLsoft has posted an updated version of the above gist, thanks man! It turns ExtendedManagedObject
int a category on NSManagedObject
(much more elegant) and deals with the traversed
flag. See his comments for details.
UPDATE 2: nuthatch has yet another improvement on that gist, check it out. Thanks nuthatch.