NSCoding Tutorial for iOS: How To Save Your App Data

An NSCoding tutorial on how to quickly and easily save your app’s data with NSCoding and NSFileManager in iOS. By Ray Wenderlich.

Leave a rating/review
Save for later
Share

This bug doesn't look so scary on disk!

This bug doesn't look so scary on disk!

There are many different ways to save your data to disk in iOS – raw file APIs, Property List Serialization, SQLite, Core Data, and of course NSCoding.

For apps with heavy data requirements, Core Data is often the best way to go.

However, for apps with light data requirements, NSCoding with NSFileManager can be a nice way to go because it’s such a simple alternative.

In this NSCoding tutorial, we’re going to take the “Scary Bugs” app that we’ve been working on in the How To Create A Simple iPhone App Tutorial Series and extend it so that it saves the app’s data to disk.

Along the way, we’ll cover how you can use NSCoding to persist your normal app data, and use NSFileManager to store large files for efficiency.

If you don’t have the project already, grab a copy of the Scary Bugs project where we left off last time.

Implementing NSCoding

NSCoding is a protcol that you can implement on your data classes to support encoding and decoding your data into a data buffer, which can then be persisted to disk.

Implementing NSCoding is actually ridiculously easy – that’s why I find it so helpful to use sometimes. Watch how quickly we can bang this out!

First make the following mod to ScaryBugData.h:

// Modify @interface line to include the NSCoding protocol
@interface ScaryBugData : NSObject <NSCoding> {

Then add the following to the bottom of ScaryBugData.m:

#pragma mark NSCoding

#define kTitleKey       @"Title"
#define kRatingKey      @"Rating"

- (void) encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:_title forKey:kTitleKey];
    [encoder encodeFloat:_rating forKey:kRatingKey];
}

- (id)initWithCoder:(NSCoder *)decoder {
    NSString *title = [decoder decodeObjectForKey:kTitleKey];
    float rating = [decoder decodeFloatForKey:kRatingKey];
    return [self initWithTitle:title rating:rating];
}

That’s it! Note that we have to implement two methods: encodeWithCoder, and initWithEncoder. I like to think of encodeWithCoder as “encode”, and initWithCoder as “decode”.

In encodeWithCoder, we’re passed in a NSCoder object, and we can call helper methods on it to encode various pieces of data. It has methods such as encodeObject, encodeFloat, encodeInt, etc. Each time we encode something, we provide a key that we can use to decode the object in initWithCoder later.

The nice thing about the way NSCoder is set up is that it makes it easy to modify your objects over time as you release new versions of your app and still be able to support older files.

One of the most common things you’ll do in your apps is add new fields to your objects. When you have a new field, you can encode it in encodeWithCoder, and in decodeWithCoder you can try decoding it, but check if it’s nil (for objects) or 0 (for numerics), which means “that key was not found”. And if it’s not found, you can provide a default value.

If 0 or nil are actually valid values, you can also take the approach of serializing a version number along with your data and use that to understand what should be there or not (in fact that’s the approach I usually take).

If you want to read up more on NSCoding, I recommend this great article by Mike Ash on implementing NSCoding.

Loading and Saving To Disk

We’ve implemented the code to encode and decode our data, but we still need some code to load and save that to disk.

We’re going to approach this by adding a new initializer to our ScaryBugDoc that takes a directory name to look in for data to load. However for efficiency purposes we won’t load the data in right away – we will load it the first time it’s accessed, by implementing the “get data” method.

We’ll also provide a method that we can call to save the document back out to disk, a method to delete the document, and an initializer that doesn’t take a document path at all, for the case where we are making a new file so will need a new directory to save the file in.

You may wonder why we’re taking a directory path as an argument rather than just a single file path. This is because we’re going to eventually have three pieces of data we’re saving in this directory – the encoded ScaryBugData, the thumbnail image, and the full image. We’ll cover why we’re taking this approach and how to save the images out later on.

Ok so let’s do this! Make the following changes to ScaryBugDoc.h:

// Inside @interface
NSString *_docPath;

// After @interface
@property (copy) NSString *docPath;
- (id)init;
- (id)initWithDocPath:(NSString *)docPath;
- (void)saveData;
- (void)deleteDoc;

Next we need to make some changes to ScaryBugDoc.m. There’s several bits here that we need to discuss, so let’s go over it bit by bit.

1) Add initializer and bookkeeping code

// At top of file
#import "ScaryBugDatabase.h"
#define kDataKey        @"Data"
#define kDataFile       @"data.plist"

// After @implementation
@synthesize docPath = _docPath;

// Add to dealloc
[_docPath release];
_docPath = nil;

// Add new methods
- (id)init {
    if ((self = [super init])) {        
    }
    return self;
}

- (id)initWithDocPath:(NSString *)docPath {
    if ((self = [super init])) {
        _docPath = [docPath copy];
    }
    return self;
}

First, we include a file we haven’t written yet – ScaryBugDatabase.h. We’ll be writing that next, so don’t worry about it for now.

Next we make some definitions for the key we’re going to save out data out to and the filename, which we’ll use a bit later.

We synthesize our new property and remember to release it in dealloc.

Finally, we write our two new initializers. Regular init does just about nothing, and initWithDocPath sets our docPath instance variable based on the value passed in.

Note that for both init and initWithTitle, docPath will be nil. When docPath is nil, that means to us that this document hasn’t been saved to disk yet, so when we save we’ll need to find a new location to save it in.

2)Write helper function to create document path

- (BOOL)createDataPath {
    
    if (_docPath == nil) {
        self.docPath = [ScaryBugDatabase nextScaryBugDocPath];
    }
    
    NSError *error;
    BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:_docPath withIntermediateDirectories:YES attributes:nil error:&error];
    if (!success) {
        NSLog(@"Error creating data path: %@", [error localizedDescription]);
    }
    return success;
    
}

When we go to save our document, the first thing we’re going to want to do is create the directory to save it in if it doesn’t exist already.

To find an unused directory, we’re going to use the ScaryBugDatabase helper class which we’ll write next to figure that out.

Once we have an unused directory, we can use the createDirectoryAtPath method in NSFileManager to create it for us. It will return success if it creates the directory, or if it already exists.

3) Override data property to load from disk

- (ScaryBugData *)data {
 
    if (_data != nil) return _data;
    
    NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
    NSData *codedData = [[[NSData alloc] initWithContentsOfFile:dataPath] autorelease];
    if (codedData == nil) return nil;
                
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
    _data = [[unarchiver decodeObjectForKey:kDataKey] retain];    
    [unarchiver finishDecoding];
    [unarchiver release];
    
    return _data;
        
}

When someone tries to access the data property, we’re going to check if we have it loaded into memory – and if so go ahead and return it. But if not, we’ll load it from disk!

The first thing we do is create the full path to the file by appending the kDataFile constant (from the top of our file – “data.plist”) to our directory name, and then loading the data from disk with NSData’s initWithContentsOfFile method.

Next we need to unserialize the data. The way you do that is by using the NSKeyedArchiver class, passing in the NSData buffer, and then call decodeObjectForKey. Behind the scenes, it will detect that the data buffer contains your ScaryBugDoc, call the initWithCoder method on that to initialize the class, and return you the new object.

4) Add method to save to disk

- (void)saveData {
    
    if (_data == nil) return;

    [self createDataPath];
    
    NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];          
    [archiver encodeObject:_data forKey:kDataKey];
    [archiver finishEncoding];
    [data writeToFile:dataPath atomically:YES];
    [archiver release];
    [data release];
    
}

In saveData we do the reverse of the above. First we call our createDataPath method to create the directory if it doesn’t already exist, then we serialize the data with NSKeyedArchiver and write it out to disk.

5) Add method to delete doc

- (void)deleteDoc {
    
    NSError *error;
    BOOL success = [[NSFileManager defaultManager] removeItemAtPath:_docPath error:&error];
    if (!success) {
        NSLog(@"Error removing document path: %@", error.localizedDescription);
    }
    
}

Last more thing to add. Now that we’re actually saving data out to disk, if the user deletes a bug from the table view, we’ll also need to delete the data from the disk, so we need a method for that:

Very simple stuff here – just a call to removeItemAtPath, which will remove the entire directory plus its contents.

Ok we’re getting pretty close! We’re just missing two pieces now: the ScaryBugDatabase object, and integration into the rest of our app.