NSCoding: Painlessly Storing Your Data

If you’ve ever needed to save dictionaries within your apps, you are undoubtedly aware of their biggest limitation: you can only store generic data types (NSString, NSData, NSDate, NSNumber, NSArray, NSDictionary) within them. To be honest, most of the time those generic data types are all you need; but what if you need to do something a little more… robust? This is where NSCoding comes to the rescue.

NSCoding: What is it?


NSCoding is a protocol that defines the methods needed to give a class encode/decode-ability. In short, it allows you to store an instance of a class to disk as a simple chunk of data. When used in tandem with dictionaries, this can become an invaluable (and supremely time-efficient) method of storing data in your application.

So How Do I Use it?


Making a class NSCoding compliant is actually incredibly easy. You only need to implement two methods: encodeWithCoder: and initWithCoder:

Let’s take a look at a simple example:

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject <NSCoding>

@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSInteger age;

- (Person *)initWithName:(NSString *)name age:(NSInteger)age;

@end

Person.m

#import "Person.h"

@implementation Person

- (Person *)initWithName:(NSString *)name age:(NSInteger)age
{
    if(self = [super init]) {
        _name = name;
        _age = age;
    }
    return self;
}

#pragma mark NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder {

    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeInteger:_age forKey:@"age"];

}

- (id)initWithCoder:(NSCoder *)aDecoder {

    NSString *name = [aDecoder decodeObjectForKey:@"name"];
    NSInteger *age = [aDecoder decodeIntegerForKey:@"age"];
    return [self initWithName:name age:age];

}

@end

Alright so what’s happening here? Well it’s pretty straightforward, encodeWithCoder: is responsible for encoding your class’s variables. For every variable you wish to encode and store, pass it into NSCoder’s respective encode method (i.e. encodeObject:forKey: , encodeInteger:forKey: , encodeBool:forKey: , etc …).

Naturally, initWithCoder: is responsible for decoding your stored objects and returning a new instance of your class. Again, for every variable stored with encodeWithCoder: that you wish to access, you retrieve it by it’s key via NSCoder’s respective decode method (i.e. decodeObjectForKey: , decodeIntegerForKey: , decodeBoolForKey: , etc …).

The Person class is now NSCoding compliant! Woot woot! Alright so how do we actually save and retrieve our stored classes? Let’s take a closer look at NSKeyedArchiver and NSKeyedUnarchiver.

Storing Data With NSKeyedArchiver


Alright so we have our fancy NSCoding compliant Person class, let’s store some Person objects to disk!

- (void)archivePeople {

    Person *john = [[Person alloc] initWithName:@"John Smith" age:27];
    Person *steve = [[Person alloc] initWithName:@"Steve Jobs" age:56];
    Person *jeff = [[Person alloc] initWithName:@"Jeff Jefferson" age: 42];

    NSArray *people = @[john, steve, jeff];
    NSData *peopleData = [NSKeyedArchiver archivedDataWithRootObject:people];

    NSDictionary *dictionary = @{@"people" : peopleData};
    if([dictionary writeToFile:@"/path/to/People.plist" atomically:YES]) { NSLog(@"Successfully saved people!"); }
    else { NSLog(@"Error saving people"); }

}

We create some instances of our People class, add them to an array, convert the array to an NSData object via NSKeyedArchiver’s archivedDataWithRootObject: method, create and add our people data to an NSDictionary, then save the dictionary to the disk! A little bit of work, but not too complicated when weighed against the benefits we get out of it.

Retrieving Data with NSKeyedUnarchiver


We’ve successfully stored some data, now let’s retrieve it!

- (void)retrievePeople {

    NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:@"/path/to/People.plist"];

    NSData *peopleData;
    if(dictionary) { peopleData = dictionary[@"people"]; }
    else { NSLog(@"Error loading people"); return; }

    NSArray *people = [NSKeyedUnarchiver unarchivedDataWithObject:peopleData];
    for(Person *person in people) {
        NSLog(@"Loaded person:%@ age:%ld", p.name, (long)p.age);
    } 

}

If everything went as planned, you should see the following in your log:

Loaded person:John Smith age:27
Loaded person:Steve Jobs age:56
Loaded person:Jeff Jefferson age:42

Wrapping Up


If you plan on storing instances of your classes to disk, complying to the NSCoding Protocol can be a real lifesaver. For simple classes, it may be a little difficult to see the benefits; but for more complex classes that host a number of variables needing persistence, it can be a godsend.