iOS Persistence : NSCoding in Objective-C and in Xamarin.iOS-Monotouch

Darrell Kress / Wednesday, October 16, 2013

Introduction

Applications store data.  As a statement that is about as noncontroversial as it comes.  How to store data on different platforms can result in long, involved and often boring conversations if the chatter my coworkers and I have on the way to lunch is any indication.  

The iOS libraries make saving data pretty simple though, so that is good.  If you are using Objective-C, data objects can implement the NSCoding protocol and allow the native libraries to save and load data objects.  Monotouch takes a slightly different path, but most of the steps are the similar enough that going between the two should be straightforward. Once these methods are implemented, we can use the NSKeyedArchiver and NSKeyedUnarchiver objects to save and load our our data.

Objective-C and the NSCoding protocol

The NSCoding protocol is a simple two method protocol that allows the saving and loading of data.  

NSCoding has two main parts

// method called during the save process to persist out the object's state.
-(void)encodeWithCoder:(NSCoder *)aCoder
// init called when the object is being rehydrated from persistence.
-(id)initWithCoder:(NSCoder *)aDecoder

So as a quick example let just show off a data class which implements this protocol. We make a Book class, based off NSObject and using the NSCoding protocol.

@interface Book : NSObject < NSCoding >  
@property int pageCount;
@property NSString* title;
@property Publisher* publisher;
@end
// and we implement the class as follows
@implementation Book
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.title forKey:@"title"];
[aCoder encodeInt:self.pageCount forKey:@"pageCount"];
[aCoder encodeObject:self.publisher forKey:@"publisher"];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
self.title = (NSString*)[aDecoder decodeObjectForKey:@"title"];
self.pageCount = [aDecoder decodeIntForKey:@"pageCount"];
self.publisher = (Publisher*)[aDecoder decodeObjectForKey:@"publisher"];
return self;
}
@end

The class has three properties, the first two are default types, the last a custom data type which we will get to in a bit. In the encodeWithCoder method we use the passed in argument object and attach the properties. Passing a string key value allows for later retrieval. In this case, I used the property name as the key, but that is just a personal preference. Since we control the usage of the "encode" methods, we can choose which fields get saved and how they get saved. We can also save extra information which can be used at decode time. For example, if we encode a version number

[aCoder encodeInt:1 forKey:@"version"];

And later use that version number in the decoder to take extra steps if the version has changed, but that is a discussion for another day.

So after encoding the data we need to rehydrate that data out. During that process iOS will use as special initializer to recreate objects that were saved. That init is the initWithCoder. From the above code we can see that as simple as the objects were to save, they are just as simple to recover. We call the various "decode" methods off the NSCoder object, casting when necessary, and our object is back from storage.

All this persistence magic happens pretty automagically using NSKeyedArchiver and NSKeyedUnarchiver.

NSData* _savedData;
// to save data
// the makeData method returns a NSMutableArray of book objects _savedData = [NSKeyedArchiver archivedDataWithRootObject:[self makeData]];
// to load data
// Since we saved a NSMutableArray when we rehydrate we will get a populated array back NSMutableArray* bookData = (NSMutableArray*) [NSKeyedUnarchiver unarchiveObjectWithData:_savedData];

So earlier I mentioned that we would get back to persisting an inner custom object. As long as we use the NSCoding protocol on that object as well than it's a simple call to encodeObject from the parent object. In fact that is already in the above code and in the sample code attached to this post.

Monotouch and NSCoder

Handling persistence in Monotouch is similar enough to look familiar, but different enough that it warrants a little more explanation.  We don't have the direct NSCoding protocol.  Monotouch instead provide a modified NSObject with an EncodeTo virtual method and exposing the extra constructor as an init is easy.

So lets look at how the Book class is different in Monotouch

public class Book : NSObject 
{
public int PageCount {get;set;} public string Title {get;set;} public Publisher Publisher {get;set;} public Book () { } [Export("initWithCoder:")] public Book(NSCoder coder) { this.PageCount = coder.DecodeInt (@"pageCount"); NSString str = (NSString)coder.DecodeObject (@"title"); if (str != null) { this.Title = str.ToString (); } this.Publisher = (Publisher) coder.DecodeObject ("publisher"); } public override void EncodeTo (NSCoder coder) { if (this.Title != null) coder.Encode (new NSString (this.Title), "title"); coder.Encode (this.PageCount, "pageCount"); if(this.Publisher!=null) coder.Encode (this.Publisher, "publisher"); }}

So as we can see the EncodeTo method looks really similar to the Objective-C Encode method. We had to modify it a little thought. In Monotouch we can use string primitives. When saving the data however, we need to wrap them in NSStrings so that they can be easily persisted at run time. The other change necessary was that we needed to add some extra error checking and null checking. But that is a pretty cheap and easy fix.

As for the decoding, we add another constructor and mark it to be exported it as the initWithCoder.

Again, pretty easy.

So i hope you enjoy this little walk into persistence, and hopefully next time we can find something more exciting to talk about.

Project in Xamarin Monotouch

Project in Objective-C

By Darrell Kress