Tuesday, 11 October 2011

NSKeyedArchiver Tutorial

Sometimes we may want our app to save some data before it terminates and load the data after it is opened again. There are usually two ways to achieve that:

1. using NSUserDefaults.
2. using NSKeyedArchiver.

Both are very standard methods provided by Cocoa, but as NSUserDefaults is designed for storing default settings rather than data inside the app, in most of the cases, you want to use NSKeyedArchiver instead.

To use NSKeyedArchiver, first of all you need to add a protocol called NSCoding to the class whose properties are supposed to be saved and then loaded in the app.

For example, you have a class called Student which is used to store some information about a student. Its header may look like this:
@interface Student : NSObject <NSCoding> {
    NSString *studentID;
    NSString *studentName;
}

@property (copy) NSString *studentID;
@property (copy) NSString *studentName;

+ (Student *)initWithStudentID:(NSString *)ID andStudentName:(NSString *)name;

@end
You also need to implement two delegate methods initWithCoder: and encodeWithCoder: in the .m file of this class. It may look like this:
+ (Student *)initWithStudentID:(NSString *)ID andStudentName:(NSString *)name {
    
    Student *student = [[Student alloc] init];
    
    student.studentID = [NSString stringWithString:ID];
    student.studentName = [NSString stringWithString:name];
    
    return [student autorelease];
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self setStudentID:[aDecoder decodeObjectForKey:@"studentID]];
        [self setStudentName:[aDecoder decodeObjectForKey:@"studentName"]];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:studentID forKey:@"studentID"];
    [aCoder encodeObject:studentName forKey:@"studentName"];
}
Note that you must use the same key for one object in method initWithCode: and encodeWithCoder:, or the app will not be able to find the saved object when decoding. The key is the only identifier of its corresponding object. The encoded data will be stored in a binary formatted file on your disk.

Now we are on the halfway. What we need to do then is to tell the app that we want to save some data before it is terminated. Find the applicationWillTerminate: method in YourProjectAppDelegate.m file, and add the following code:
- (void)applicationWillTerminate:(NSNotification *)notification {
    
    [self saveDataToDisk];
}
The code will call the method saveDataToDisk when user closes the app. So we're gonna implement that method to tell the app the we want to save some data. The code may look like this:
- (void)saveDataToDisk {
    NSString *path = @"~/Documents/data";
    path = [path stringByExpandingTildeInPath];
    
    NSMutableDictionary *rootObject;
    rootObject = [NSMutableDictionary dictionary];

    [rootObject setValue:student forKey:@"student"];
    
    [NSKeyedArchiver archiveRootObject:rootObject toFile:path];
}

The object student is an instance of Class Student. In this example, we save it in a file named data under the the Documents folder.

Alright, now we should be able to save our data into the disk. Now the only thing left is to load the data. The method may look like this:
- (void)loadDataFromDisk {
    NSString *path = @"~/Documents/data";
    path = [path stringByExpandingTildeInPath];
    
    NSMutableDictionary *rootObject;
    rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

    if ([rootObject valueForKey:@"student"]) {
        student = [rootObject valueForKey:@"student"];
    }
}
The method does two things: 1. Tell the app where the saved file can be found; 2. Assign object with the corresponding key to the student instance.

You can call this method whenever you believe is necessary to load the data from local disk to your app.

Happy Coding :)

7 comments:

  1. Instead of saving to a file, we can save it to user defaults... Thanks for the tutorial, btw.

    ReplyDelete
    Replies
    1. That's right. Thanks for your comment. :)

      Delete
  2. Works for me, thanks for solution.

    ReplyDelete
  3. But what if i want to save a mutable array wich contains objects of type student? Do i just encode/decode the mutable array ?

    ReplyDelete
    Replies
    1. I think NSKeyedArchiver/NSKeyedUnarchiver should work with arrays/dictionaries without any extra effort, as long as its elements support NSCoding

      Delete
    2. Ok. Thank you very much :)

      Delete