« Using the Property List Objects and the NSCoding Protocol

Posted by Submission on October 29, 2001 [Feedback (0) & TrackBack (0)]

by H. Lally Singh

(website | email)

We've all got to store persistent data at some time. For that purpose, Cocoa provides you with two basic archiving options: XML based property lists, and persistent objects.

 

// XML based Property Lists

Saving some data to an XML property list is really easy under Cocoa . The simplest way to do it is to save a property list object, which can be any of NSArray, NSDictionary, NSString, or NSData. For most things, an NSArray or NSDictionary will do. A nice feature is that since NSArray & NSDictionary can contain other property list objects, those objects will be written into the property list as well. Do the smart thing & don't ask what happens when you put something else in there too.

Say you've got some various preferences you want to save:

@interface ClientDisplayMgr {
   ...
   IBOutlet id m_clientName;   // outlets to text boxes in the preferences
   IBOutlet id m_serverName;   // window.
   NSArray *m_availableFriends;
   ...
}
@end

//
// Save some various preferences
- writePrefs
{
    NSMutableDictionary * prefs;

   // allocate an NSMutableDictionary to hold our preference data
    prefs = [[NSMutableDictionary alloc] init];

   // our preference data is our client name, hostname, and buddy list
    [prefs setObject:[m_clientName stringValue] forKey:@"Client"];
    [prefs setObject:[m_serverName stringValue] forKey:@"Server"];
    [prefs setObject:m_friends forKey:@"Friends"];
    
    // save our buddy list to the user's home directory/Library/Preferences.
    [prefs writeToFile:[@"~/Library/Preferences/MiniMessage Client.plist"
                    stringByExpandingTildeInPath] atomically: TRUE];
    return self;
}

This results in the following data being written in /Users/<username>/Library/Preferences/MiniMessage Client.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
   <key>Client</key>
   <string>Crazy Joe</string>
   <key>Friends</key>
   <array>
      <string>Crazy Joe</string>
      <string>Jim</string>
      <string>Joe</string>
      <string>Crazy Jim</string>
      <string>Jose</string>
      <string>Crazy Joe</string>
   </array>
   <key>Server</key>
   <string>localhost</string>
</dict>
</plist>

Which is in good ol' fashioned XML. Notice the <dict>, <array>, and <string> tags, which correspond directly to the objects we wrote to the file: a dictionary containing an array of strings, with some other strings on the side.

To read in the data, try something like this:

//
// When this object instance awakens from the nib file, it loads its preference
// data from property list.
- awakeFromNib
{
    NSString *clientName, *serverName;
    NSDictionary *prefs;
    
    // load the preferences dictionary
    prefs = [NSDictionary dictionaryWithContentsOfFile: 
            [@"~/Library/Preferences/MiniMessage Client.plist" 
               stringByExpandingTildeInPath]];
   
   // if the file was there, we got all the information we need.
   // (note that it's probably a good idea to individually verify objects
   //  we pull out of the dictionary, but this is example code :-)
    if (prefs) {
       //
       // write our loaded names into the preference dialog's text boxes.
      [m_clientName setStringValue: [prefs objectForKey:@"Client"]];
      [m_serverName setStringValue: [prefs objectForKey:@"Server"]];
      
      //
      // load our friend list.
      m_friends = [[prefs objectForKey:@"Friends"] retain];
    } else {
       //
       // no property list.  The nib file's got defaults for the 
       // preference dialog box, but we still need a list of friends.
      m_friends = [[NSMutableArray alloc] init];
      // we're our only friend (isn't it strange talking about we in the singular?)
      [m_friends addObject: [m_clientName stringValue]];
    }
    
    //
    // get our preference data for the rest of awakeFromNib
    clientName = [m_clientName stringValue];
    serverName = [m_serverName stringValue];
    ...
}

Put data into container, write it out. Read in container, pull data from it. Simple 'nuff.

 

// Archiving Objects

For property lists and other relatively small amounts of data, the XML format described above is probably your best bet. However, you'll probably encounter some set of objects that you'd like to be persistent, or maybe just not in such an easily readable format as XML :-)

For those reasons and many more, Cocoa has graciously provides us with an object persistence api, the NSCoding protocol and the NSCoder, NSArchiver, and NSUnarchiver classes. The procedure is pretty simple: define methods to read & write your member variables from a NSCoder. What, you thought it would be hard? This is Cocoa! Say it with me, Co-coa!!

Here, I'll give you an example to make you feel better:

//
// Archiving example
@interface ArchiveMe : NSObject <NSCoding>
{
   int   somevalue;
   NSString * str;
}
- init;
- (void) dealloc;
- (void) encodeWithCoder: (NSCoder *)coder;
- initWithCoder: (NSCoder *)coder;
@end

@implementation

//
// writes ourselves out to an NSCoder
- (void) encodeWithCoder: (NSCoder *)coder
{
   [coder encodeObject:[NSNumber numberWithInt: somevalue]];
   [coder encodeObject:str];
}

//
// reads ourself back in from an NSCoder.
- initWithCoder: (NSCoder *) coder
{
   [super init];
   somevalue = [[coder decodeObject] intValue];
   str = [[coder decodeObject] retain];
   return self;
}

//
// standard initializer
- init
{
   [super init];
   somevalue = 0;
   str = [[@"Foo" stringByAppendingString: @" Bar"] retain];
}

//
// gotta be diligent about memory management!
- (void) dealloc
{
   [str release];
   [super release];
}
@end

See, that wasn't too bad. Only unusual code there was encodeWithCoder: and initWithCoder:, and they were pretty straightforward: just encode & decode objects in the same order, using either NSCoding-implementing classes or the specialized NSCoder routines for basic data types (encodeValueOfObjCType:at:, encodeBytes:length:, etc.. see NSCoder for more information).

 

// Actually Archiving and Unarchiving Objects

To do the actual archiving work, just call NSArchiver & give it a filename:

ArchiveMe * obj;
obj = [[ArchiveMe alloc] init];
[NSArchiver archiveRootObject:obj toFile:
    [@"~/Desktop/Archived Object.archive" stringByExpandingTildeInPath]];

The code above does what it looks like it does: creates an ArchiveMe and then archives it to a file. Let's look at how to unarchive it:

obj = [[NSUnarchiver unarchiveObjectWithFile:[@"~/Desktop/Archived Object.archive" stringByExpandingTildeInPath]] retain];

 

// Conclusion

Well, that was a pretty simple introduction to the pretty simple idea of persistent objects. Now you can save your preferences and also any arbitrary set of objects with only a few function calls! Doesn't the second one just scream proprietary binary file format? MS-Word here we come!!

And in case anyone was wondering, the property list code above was taken from an instant messenging client that I've written (along with a server) to use as a basis for a set of upcoming articles, which include topics like distributed objects, loading your own nib files, and maybe even a few tips on sheets and NSTableViews if you're nice :-)


Comments
Post a comment