3

This has been asked before and people have given very good instructions on how to do this, e.g. here.

However, I was wondering if I really need to work with NSCoder if I simply wanted to save one NSMutableArray (containing various instances of another NSMutableArray) to a file? I tried this but only got an error message:

    -(void)saveLibraryDat {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"myLibrary.dat"];
    NSError *error;

    [myLibrary writeToFile:filePath atomically:YES];

    if (error) {
        NSLog(@"There was an error saving myLibrary.dat: %@", error);
    }

}

My error message:

2011-05-13 22:00:47.840 MoleNotes[15437:207] There was an error saving myLibrary.dat: (
    1,
    2
)

So I guess I have to work with NSCoder, right? If so, I was wondering how to go about this. People have explained how to do this with a class, but in my case, I have a NSMutableArray (myLibrary) which contains various instances of a class. Will I have to implement the NSCoder in this class and the NSMutableArray?

I alloc my library like this:

    myLibrary = [[NSMutableArray alloc] init];

And then add instances of a class called NoteBook.m like this:

    NoteBook *newNoteBook = [[NoteBook alloc] init];
       newNoteBook.titleName = @"Notes"; // etc.

[myLibrary addObject:newNoteBook];

So where exactly do I put the NSCoder commands? Only into my NoteBook.m class? Will this automatically take care of myLibrary?

Thanks for any suggestions.


EDIT:

So I've updated my code, but I guess the big problem is that my NSMutableArray myLibrary contains several instances of a custom class I've set up (called notebook). I have set up NSCoding for this class (and all its variables) so that I can save it and load it.

Now my app works totally fine if I create the NSMutableArray in the app (i.e. when the app is started for the very first time, no file exists), instead of loading it from disk:

    -(void) setupLibrary {

    myLibrary = [[NSMutableArray alloc] init];

    NoteBook *newNoteBook = [[NoteBook alloc] init];

    newNoteBook.titleName = @"Notes"; 
/...

If I load it from disk, it works fine as well:

-(void)loadLibraryDat {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"myLibrary.dat"];

    myLibrary = [[NSMutableArray alloc] init];  
    myLibrary = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];

    if (!myLibrary) {
        // if it couldn't be loaded from disk create a new one
        NSLog(@"myLibrary.dat empty... set up new one");


        [self setupLibrary];
        } else { NSLog(@"Loading myLibrary.dat successful."); }

    }

If I log everything which is contained in my library after loading it, everything is still fine. E.g. the following works totally fine:

    NSLog(@"%@", [[self.myLibrary objectAtIndex:0] titleName]); 

The big problem is, however, if any other method tries to access myLibrary. For instance, if I call the very same log command from another method, the app will crash and I get this error message:

    [NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510
2011-05-14 14:09:10.490 Notes[17091:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510'

This sounds to me as if myLibrary has become deallocated somehow, but I can't see why. How could this have happened? I have the feeling that I did something wrong in my NSCoding set up... because if I simply create myLibrary in code, everything works like wonderfully. It's only if I load it from the disk, that the app will crash.


Here is the class setup:

  #import <Foundation/Foundation.h>

@interface NoteBook : NSObject <NSCoding> {

    NSString *titleName;

    NSString *fileName;
    NSMutableArray *tabTitles;
    NSMutableArray *tabColours;
    NSMutableArray *tabReference;   

}

@property (nonatomic, retain) NSString *titleName;

@property (nonatomic, retain) NSString *fileName;
@property (nonatomic, retain) NSMutableArray *tabTitles;
@property (nonatomic, retain) NSMutableArray *tabColours;
@property (nonatomic, retain) NSMutableArray *tabReference;

-(id)initWithCoder:(NSCoder *)aDecoder;
-(void)encodeWithCoder:(NSCoder *)aCoder;


@end


//
//  NoteBook.m

#import "NoteBook.h"


@implementation NoteBook

@synthesize titleName, fileName, tabTitles, tabColours, tabReference;

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        self.titleName = [aDecoder decodeObjectForKey:@"titleName"];
        self.fileName = [aDecoder decodeObjectForKey:@"fileName"];
        self.tabTitles = [aDecoder decodeObjectForKey:@"tabTitles"];
        self.tabColours = [aDecoder decodeObjectForKey:@"tabColours"];
        self.tabReference = [aDecoder decodeObjectForKey:@"tabReference"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:titleName forKey:@"titleName"];
    [aCoder encodeObject:fileName forKey:@"fileName"];
    [aCoder encodeObject:tabTitles forKey:@"tabTitles"];
    [aCoder encodeObject:tabColours forKey:@"tabColours"];
    [aCoder encodeObject:tabReference forKey:@"tabReference"];
}

@end

EDIT:

I think I've solved it... I forgot a little 'self'... which messed it all up and deallocated myLibrary:

    self.myLibrary = [NSKeyedUnarchiver
                   unarchiveObjectWithFile:filePath];

if (self.myLibrary == nil) {
    NSLog(@"myLibrary.dat empty... set up new one");
    [self setupLibrary];
} else { NSLog(@"Loading myLibrary.dat successful."); }
Community
  • 1
  • 1
n.evermind
  • 11,944
  • 19
  • 78
  • 122
  • I think you should consider XML plists or JSon, but I admit I don’t remember what NSCoder does, just that I never used it to save my NSArrays. Was something like [array writeToFile:.... – gurghet May 14 '11 at 08:39
  • 1
    `writeToFile` should work as long as your objects are plist compliant (that is, they are strings, arrays, numbers and such). To store custom classes, you must use NSCoder and marshall them yourself. – Martin Wickman May 14 '11 at 10:17
  • @Martin Wickman. Thanks, I've tried ti implement this, but I guess I must have went somewhere completely wrong. Loading seems to work, but if I try to access my variable, the app will crash... see my edited post. Thanks so much for your help! – n.evermind May 14 '11 at 13:41
  • @Martin Wickman: Sorry to have bothered, I solved it myself. Thanks! – n.evermind May 14 '11 at 13:52

2 Answers2

5

Your code is busted. The "error" variable is uninitialized and never set, so when you check it, you're just seeing random garbage data. If you want to know whether the write was successful, check the return value of writeToFile:atomically:. It will be YES if the write succeeded and NO if it didn't.

However, NSArray's writeTo… methods are for creating plists. If non-property-list objects are in your array, that method isn't appropriate, and an archiver is what you want. Just do something like [[NSKeyedArchiver archivedDataWithRootObject:myLibrary] writeToFile:writeToFile:filePath atomically:YES].

To make your objects conform to NSCoding correctly, just have them implement initWithCoder: and encodeWithCoder:, and in those methods, use NSCoder's storage methods to store the object's instance variables (and the retrieval methods to get them back out).

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • I tried `BOOL succeed = [myLibrary writeToFile:filePath atomically:YES]; if (!succeed){ NSLog(@"There was an error saving myLibrary.dat: %@", error); }` but that returned (nil)... I guess I don't get the whole idea of checking for errors, I'm afraid. – n.evermind May 14 '11 at 08:50
  • Thanks, I did all of this (incl. implementation of NSCoder), but I still get (null) for my error check: `BOOL succeed = [[NSKeyedArchiver archivedDataWithRootObject:myLibrary] writeToFile:filePath atomically:YES]; if (!succeed){ NSLog(@"There was an error saving myLibrary.dat: %@", error); }` – n.evermind May 14 '11 at 09:11
  • OK, I've tried to implement everything you suggested, but I think I'm still doing something wrong as my app crashed if I try to access the NSMutableArray I tried to save. See my updated code above. Thanks so much for your help! – n.evermind May 14 '11 at 13:35
1

NSCoder is a protocol that your class must conform to in order to be archived to data/file. Works something like Serealizabe in Java.

Add conformance to the class header like this:

@interface NoteBook : NSObject <NSCoder> { // …

And then you must implement two methods:

-(id)initWithCoder:(NSCoder)decoder;
{
    self = [super initWithCoder:decoder];
    if (self) {
        _someIvar = [decoder decodeObjectForKey:@"someKey"];
        // And more init as needed…
    }
    return self;
}
-(void)encodeWithCoder:(NSCoder)coder;
{
   [super encodeWithCoder:coder];
   [coder encodeObject:_someIvar forKey@"someKey"];
   /// Etc…
}

I would also advice against using -[NSArray writeToFile:atomically:] since in work with property list compliant objects only, not coding compliant classes. The property list object are NSString, NSData, NSArray, or NSDictionary, NSDate, and NSNumber. The list can not be extended.

Instead use NSKeyedArchiver/NSKeyedUnarchiver. Almost as simple to use:

if (![NSKeyedArchive archiveRootObject:yourArrat toFile:path]) {
  // It failed.
}
PeyloW
  • 36,742
  • 12
  • 80
  • 99
  • OK, I've tried to implement everything you suggested, but I think I'm still doing something wrong as my app crashed if I try to access the NSMutableArray I tried to save. See my updated code above. Thanks so much for your help! – n.evermind May 14 '11 at 13:35