2

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
halfer
  • 18,701
  • 13
  • 79
  • 158
Maggie
  • 7,253
  • 6
  • 39
  • 65

2 Answers2

0

Is there a reason why you don't just save the NSData object directly to a file with

data writeToFile:(NSString*)path atomically:(BOOL)flag

?

I don't think you get an error in your NSPropertyList call - the propertyListWithData function returns nil if an error occurred, and I suppose the error is valid only then. You probably should check on return value != nil, and then print the error. Error 3840 marks the beginning of the range of property list errors, that doesn't look like an error in itself.

The description of propertyListWithData also says that it returns a property list, not an array, so it's not surprising that isKindOfClass says it's not an array ... see here:

http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSPropertyListSerialization_Class/Reference/Reference.html

TheEye
  • 8,809
  • 2
  • 38
  • 56
0

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?

mackworth
  • 5,655
  • 2
  • 24
  • 49
  • This is what I ended up doing. I am still wondering why the above code isn't working. – Maggie Oct 24 '11 at 22:09
  • 1
    Don't know, but a few possible paths: First, I don't have your XML, but my guess is it's coming in as a NSDictionary, not an NSArray. NSLog(@"data class: %@",[[data class] description]) to check. Second, you can get more useful info from error. See second answer here: http://stackoverflow.com/questions/1283960/iphone-core-data-unresolved-error-while-saving/1297157 Third, you may be mixing up XML and keyed-archives. NSKeyedUnarchive, AFAIK, can only read data written by NSKeyedArchive, not arbitrary XML. Finally, I don't think you need a coder for an object with only basic classes. – mackworth Oct 25 '11 at 05:30
  • I doesn't seem to be a NSDictionary either... anyway, thanks for your help. the error I'm getting is: NSDebugDescription = "Encountered unknown tag currencies on line 2"; kCFPropertyListOldStyleParsingError = "Error Domain=NSCocoaErrorDomain Code=3840 \"The operation couldn\U2019t be completed. (Cocoa error 3840.)\" (Malformed data byte group at line 1; invalid hex) UserInfo=0x625bc70 {NSDebugDescription=Malformed data byte group at line 1; invalid hex}"; – Maggie Oct 25 '11 at 20:52
  • did you try the NSLog(@"data class: %@",[[data class] description]) ? Looks like you got good further info: "unknown tag currencies on line 2" (Don't worry about kCFProp...It tries again for non-XML old-style plist format, so it should fail). What does start of XML look like? – mackworth Oct 25 '11 at 23:04
  • here is the start of my XML: 3.6732 – Maggie Oct 28 '11 at 17:15