Pages

Friday, April 29, 2011

Saving Objects with NSCoder and NSCoding

The iOS API provides many ways to save data to a device. Some of the most common methods are CoreData, NSUserDefaults, and property lists. One lesser know, but very useful method,  is using NSCoder and the NSCoding Protocol.  NSCoder and the NSCoding Protocol are useful because they will allow you to save instances of objects to the device.

This tutorial is the Hello World for NSCoder and NSCoding. I will show you how to make a simple application that will save a Users name into a custom User object and then present it back to the user whenever the application is started.

Start by creating a new window based application, add in a RootViewController with an UITextField, UIButton, and make all the Interface Builder connections. Than create a new file call User that is a subclass of NSObject.

We will start in the User.h file.

First create a macro for the name of the file that we will save the data to:


#define FILE_NAME   @"UserInfo"

Then make sure the User object conforms to the NSCoding Protocol by adding <NSCoding> to the @interface line and add an instance variable for the user's name:



@interface User : NSObject <NSCoding> {
    NSString *mUserName;
}


Add a property for the user's name:

@property (nonatomic, retain) NSString *userName;

Lastly define a default init method:



- (id)initWithName:(NSString *)name;

Now on to where the magic happens, the User.m file.


First we need to synthesis the userName property:


@synthesize userName=mUserName;


We will start with the default init method. This method prepare to save a the object if needed, return a saved object if one exists, or create a new object.


Start with the standard init method implementation:


- (id)initWithName:(NSString *)name {
    self = [super init];
    if (self) {


If an NSString is passed through the name parameter, that means the user has entered a name in the UITextField. So it needs to be saved.   Simply check to see if there is a name, if so set to the mUserName instance and return the object:



        if (name) {
            mUserName = name;
        }

If the name is nil, then we need to looked for a User object has been saved to the device. The first step to doing that is finding the path file in Documents Directory:


        else {
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,          NSUserDomainMask, YES);
            NSString *directory = [paths objectAtIndex:0];
        
            NSString *path = [[NSString alloc] initWithFormat:@"%@/%@", directory, FILE_NAME];

After finding the path we can us NSFileManager to check if the file exists:

            if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {

If a file exists create a new object from the data in that file. This is object that will be returned. Here is how that done:


                [self release];
            
                NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData dataWithContentsOfFile:path]];
            
                self = [[User alloc] initWithCoder:unarchiver];
            
                [unarchiver finishDecoding];
                [unarchiver release];
            }

The first thing we do is call [self release];. While this looks odd it is correct memory management, because alloc is called on the User object when it is initialized. When it is found that this object has been saved we create a whole new object from that file. As you can see alloc is called on the User object again in this line: self = [[User allocinitWithCoder:unarchiver];. If we did release the previous on there would be an extra User object float around, i.e. a leak. Next we create a NSKeyedUnarchiver instance and pass the data from the saved file to it using the dataWithContentsOfFile: factory method of the NSData class. After that a new User object is created using the initWithCoder: method (I will go over this implementation later). The next step is call [unarchiver finishDecoding]; This method must be called for the NSKeyedUnarchiver to complete. Finally release the unarchiver.

The last thing done with the initWithName: method is to set a default value to mUserName if a User object has not been saved, and return the object.


            else {
                mUserName = @"Default User";
            }
            [path release];
        }
    }
    return self;
}

Now The NSCoding protocol method need to be implemented. We will start with the initWithCoder: method:

- (id)initWithCoder:(NSCoder *)aDecoder {
    mUserName = [aDecoder decodeObjectForKey:@"Name"];
    return self;
}

This is pretty simply it just sets the mUserName variable to the object that was encoded with the "Name" key. Notice that this method does not include a [super initWithCoder:aDecoder]; method call. That is because NSObject does not conform to the NSCoding protocol. If your super class does conform to the NSCoding protocol you should call [super initWithCoder:aDecoder];.

The other method used by the NSCoding protocol is encodeWithCoder:


- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:mUserName forKey:@"Name"];
}

This method encodes the mUserName variable with the "Name" key. Again if the super class conforms to the NSCoding protocol you need to call [super encodeWithCoder:aCoder];.

Thats it for making the object that will be saved. Now lets go on to using this class in the RootViewController. The RootViewController should have a UITextField, a UIButton, and have the User.h file imported. 


When the app launches we want to say "Hello" to the user so add this following to the viewDidLoad: method:



    User *user = [[User alloc] initWithName:nil];
       
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" message:user.userName delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alert show];
    [alert release];
    
    [user release];

The User object is pretty simple to implement. The initWithName: method takes care of all the work.
When we want to say hello to the user by name call User *user = [[User allocinitWithName:nil];. 
Make sure you pass nil if you want to retrieve a name. If you pass a string with the init method the User objects assumes you want it to be saved.

Saving a User object takes a little more work. Place the follow in your method that is called when the save button is pressed:

if ([nameField.text length] > 0) {
        NSMutableData *data = [NSMutableData data];
        User *user = [[User alloc] initWithName:nameField.text];

Start by making sure a name was entered in the text field. Then you need to make a blank NSMutableData object, this is data the User object will be encoded to. Next create an instance of the User object and pass the name that was entered.

The Next part is very similar to the decoding of the User object. The difference is we use NSKeyedArchiver and the encodeWithCoder: method. Again make sure you call finishEncoding on the NSKeyedArchiver instance:

        NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
        [user encodeWithCoder:archiver];
        [archiver finishEncoding];
        
        [user release];

Now the User object has been coded into a data object so we just have to save it:

        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *directory = [paths objectAtIndex:0];
        
        NSString *path = [[NSString alloc] initWithFormat:@"%@/%@", directory, FILE_NAME];
        [data writeToFile:path atomically:YES];

After the object is saved, all that is left is clean up:

        [archiver release];
        [path release];
        
        [nameField resignFirstResponder];
        
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Name Saved" message:nameField.text delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
        [alert release];

This should be your final result.


When app launches it will say Hello to the Default User.


Enter a name and tap the save button. Then close the app and remove it from the multitasking bar.


The next time the app is launched it will say hello to user by name.

The complete project for this demo is available from github, here

Follow me on twitter @matt62king.

Happy Coding.