Ios – NSData to NSArray error in iPhone

iosiphoneobjective c

I am using NSURLConnection to fetch XML data from the server. I am parsing the data and showing it in a tableview. It all works as expected. Now, I would like to save downloaded data for an offline use. The idea was to take downloaded NSData, convert it to NSArray and store it either to NSUserDefaults or in a separate file. However, I am having problems converting NSData to NSArray.

I added the logic to (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data method. What I am trying to do is as follows:

NSError *error;
NSPropertyListFormat plistFormat;
id object = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:&plistFormat error:&error];
if (error != nil) {
   NSLog(@"Error %@", [error localizedDescription]);
   [error release];
}
if ([object isKindOfClass:[NSArray class]]) {
     NSLog(@"IS Array");
     NSArray *objectArray = object;
     [[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver  archivedDataWithRootObject:objectArray] forKey:@"myKey"];
} else {
     NSLog(@"Not an array");
}

In log I get as follows:

Error The operation couldn’t be completed. (Cocoa error 3840.)

Not an
array

If I remove error handling and leave just the line

NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:data];

my application crashes with the following message: `

Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '*** -[NSKeyedUnarchiver
initForReadingWithData:]: incomprehensible archive (0x3c, 0x3f, 0x78,
0x6d, 0x6c, 0x20, 0x76, 0x65)'

Why is this happening? What is Cocoa error 3840?

My object implements NSCoding protocol, and has methods encodeWithCoder and initWithCoder. Does every property of my object has to be encoded / decoded?

Edit: Here is my object:
Currency.h

@interface Currency : NSObject<NSCoding>{
    CGFloat value;
        NSString *code;
    NSDate *date;
    NSString *description;
    NSString *imagePath;
}
@property (nonatomic, assign) CGFloat value;
@property (nonatomic, retain) NSString *code;
@property (nonatomic, retain) NSDate *date;
@property (nonatomic, retain) NSString *description;
@property (nonatomic, retain) NSString *imagePath;

Currency.m

@implementation Currency

@synthesize value;
@synthesize code;
@synthesize date;
@synthesize description;
@synthesize imagePath;

static NSString * const keyCode = @"code";
static NSString * const keyDescription = @"description";
static NSString * const keyValue = @"value";

- (void)dealloc {
    [code release];
    [date release];
    [description release];
    [imagePath release];
    [super dealloc];
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    if ([coder allowsKeyedCoding]) {
       [coder encodeObject:code forKey: keyCode];
           [coder encodeObject:description forKey: keyDescription];
           [coder encodeFloat:value forKey: keyValue];
        }
}

}

- (id)initWithCoder:(NSCoder *) coder {
    self = [[Currency alloc] init];
    if (self != nil)
    {
        code = [[coder decodeObjectForKey:keyCode] retain];
        description = [[coder decodeObjectForKey:keyDescription] retain];
        value = [coder decodeFloatForKey:keyValue];
    }   
    return self;
}

@end

Best Answer

Revised answer:

So, there's three different concepts here: Generic XML, propertyLists (in either binary or XML format), and an Archive. You parse XML with an NSXMLParser or other library to convert into Obj-C objects. You can save core Obj-C objects (NOT including your custom class Currency) into property lists and read them back. Or you can archive any object/object graph (using encoders) into an archive and read it back with NSKeyedUnarchiver.

Even though underneath they may share implementations, you can't mix them. For example, first you tried to read XML as a propertyList, and even though pLists are XML, your generic CurrencyData XML doesn't have the right format for a plist (e.g. no ). In addition, even if you were to write a plist out, you'd have to convert Currency to a NSDictionary in order to make it storable in a plist, which means you wouldn't need the encoders.

Then you tried to read XML with NSKeyedUnarchiver and got the message "incomprehensible archive". Also correct, as it wasn't created with NSKeyedArchive.

So you can't use the original stream and directly plist or unarchive into your objects. But to save the parsed XMLArray for offline use, you can either convert Currency into an NSDictionary and use property lists, or just leave it as it is and use NSKeyedArchive and Unarchive like this (which leverages the encoders/decoders you've provided):

//To save
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:xmlArray];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"myKey"]; //or store in another file

//To later read
NSData * data2 = [[NSUserDefaults standardUserDefaults] valueForKey:@"myKey"];
NSArray * newXMLArray = [NSKeyedUnarchiver unarchiveObjectWithData:data2];

Hope that clears it up. Investigating it also cleared up my own understanding of when to use which technique.

//Original comment: If you've already parsed the incoming data into a model for your tableView, why not serialize that copy of the data rather than the original stream?