Vladimir Stazhilov
Vladimir Stazhilov

Reputation: 1954

Write complex object to file objective-c

I find it hard to write/read array of custom objects. In my my app, Contact class has a NSDictionary as property and this dictionary has array as objects for keys. I serialize/deserialize my objects with NSCoder and NSKeyedArchiever and even tried NSPropertyList serialization. I always get errors when serializing as soon as it starts to serialize NSDictionary. Here is my code and I didn't really find a general answer regarding how to serialize custom objects with complex structure?

//Contact.m
//phoneNumbers is a NSDictionary
#pragma mark Encoding/Decoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
    NSLog(@"Encoding");
    [aCoder encodeObject:self.firstName forKey:@"firstName"];
    NSLog(@"First name encoded");
    [aCoder encodeObject:self.lastName forKey:@"lastName"];
    NSLog(@"Last name encoded");
    [aCoder encodeInt:self.age forKey:@"age"];
    NSLog(@"Age encoded");

    NSString *errorStr;
    NSData *dataRep = [NSPropertyListSerialization dataFromPropertyList:self.phoneNumbers
                                                                 format:NSPropertyListXMLFormat_v1_0
                                                       errorDescription:&errorStr];
    NSLog(@"Data class %@", [dataRep class]);
    if(!dataRep)
    {
        NSLog(@"Error encoding %@", errorStr);
    }

    [aCoder encodeObject:dataRep forKey:@"phones"];

    NSLog(@"Encoding finished");

}


- (id) initWithCoder: (NSCoder *)coder
{
    if (self = [super init])
    {
        [self setFirstName:[coder decodeObjectForKey:@"firstName"]];
        [self setLastName:[coder decodeObjectForKey:@"lastName"]];

        [self setAge:[coder decodeIntForKey:@"age"]];

        NSString *errorStr;
        NSData *data=[coder decodeObjectForKey:@"phones"];
        NSDictionary *propertyList = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:&errorStr];
        if(!propertyList)
        {
            NSLog(@"Error %@", errorStr);
        }

        [self setPhoneNumbers:propertyList];
    }
    return self;
}

//Serializing/Deserializing an array of Contact objects:
#pragma mark Import/Export

//Export Contacts to file
-(void)exportContactsToFile
{
    BOOL done=[NSKeyedArchiver archiveRootObject:self.contacts toFile:[PathUtility getFilePath:@"phonebook"]];
    NSLog(@"Export done: %i", done);
}

//Import Contacts from file

-(void)importContactsFromFile
{

    self.contacts = [NSKeyedUnarchiver unarchiveObjectWithFile:[PathUtility getFilePath:@"phonebook"]];
}

Is there a generic good way to serialize/deserialize objects in objective-c? thanks The error I get is: 0objc_msgSend 1 CF_Retain ... that's stack trace, but I get no other errors(

Upvotes: 0

Views: 2347

Answers (2)

Lukman
Lukman

Reputation: 19164

I have a special class in my proprietary library that automatically reads the list of its properties and use the getter and setter to encode and decode the object. Sorry I can't share the code here but I can at least give you steps by steps how my class works:

  1. First, the class must be implement NSCoding and NSCopying protocols.

  2. Inside + (void)initialize, iterate thru the definitions of the properties of the class using class_copyPropertyList(), property_getName() and property_copyAttributeList(). Refer Objective-C Runtime Programming Guide for details on these functions.

  3. For each property, run thru its attribute list and get the attribute with strncmp(attribute.name, "T", 1) == 0 (yup, it's a c-string in there). Use that attribute value to determine the type of the property. For example, "i" means int, "I" means unsigned int, if it starts with a "{" then it's a struct etc. Refer this page on the Type Encoding.

  4. Store the property name-type pairs inside a NSDictionary. At the end of properties iteration, store this dictionary inside a static and global NSMutableDictionary using the class name as the key.

  5. To support auto-encoding, implement - (void)encodeWithCoder:(NSCoder *)aCoder to iterate thru the property name-type pair, calling the property getter method (usually - (returnType)propertyName) and encode it inside the coder using appropriate encodeType: method (e.g. encodeInt:, encodeFloat:, encodeObject:, encodeCGPoint: etc).

  6. To support auto-decoding, implement - (id)initWithCoder:(NSCoder *)aDecoder to iterate thru the property name-type pair, decode it from the decoder using appropriate decodeTypeForKey: method (e.g. decodeIntForKey:, decodeFloatForKey:, decodeObjectForKey:, decodeCGPointForKey: etc). and call the property setter method (usually - (void)setPropertyName:).

  7. Implement an instance method that trigger the encoding (luckily I can share this method here ^__^):

    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *arc = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [arc encodeRootObject:self];
    [arc finishEncoding];
    [arc release];
    return data;
    
  8. Once you have the NSData you can anything with it such as calling writeToFile:atomically: or even [[NSUserDefaults standardUserDefaults] setObject:[self wrapInNSData] forKey:key].

  9. Also, implement a class method that returns a new instance of the object loaded from the file:

    NSKeyedUnarchiver *unarc = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData dataWithContentsOfFile:dataFilePath]];
    MyCoolFileDataStore *data = [unarc decodeObject];
    [unarc release];
    return data;
    
  10. Finally, to make another object class supports this auto-encoding-decoding, the class needs to extend the special class.

Sorry, it's a bit long winded, but for my case, the extra trouble that I took to create this class really save a lot of time along the road. Struggle today, breeze through tomorrow ;)

Upvotes: 3

Patrick
Patrick

Reputation: 515

You shouldn't need to use NSPropertyListSerialization for self.phoneNumbers. NSDictionary adheres to the NSCoding protocol.

So, [aCoder encodeObject:self.phoneNumbers forKey:@"phones"]; should be sufficient.

As long as a class adheres to NSCoding (which nearly all Apple-provided class do), you can just use -encodeObject:forKey:, since that method will call that object's implementation of -encodeWithCoder:

Upvotes: 3

Related Questions