macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Mac OS X's Preferences System (and More!)
Pages: 1, 2, 3

User Defaults

As an introduction to our next topic of discussion we're going to talk about another way we can save the address book data using OS X's preferences system. As you are undoubtedly aware of by now, Mac OS X is a multi-user system to its core, unlike OS 9. As such, each user has their own set of preferences for each application he uses, and Cocoa makes accessing and managing user preferences a painless task with the NSUserDefaults class.



NSUserDefaults provides a programmatic interface to Mac OS X's preferences system. Defaults are stored in a user's preferences database as key-value pairs in a data dictionary. Thus, working with preferences is fundamentally no different than how we learned to work with data dictionaries (NSDictionary). Preference values are stored using the NSUserDefaults method setObject:forKey: (among others), and you can later access defaults using the objectForKey: method.

To create an NSUserDefaults object, you send the class object a standardUserDefaults message, which returns the object we use to interact with the preferences database for the current user of application. Let's modify our code so that the records array is initialized from the user's defaults database, rather than from an arbitrary file. To start this we have to declare an NSUserDefaults instance variable in Controller.h:

NSUserDefaults *prefs;

Simple enough. The next step is to go back to awakeFromNib and do something analogous to what we did above initializing records from a file. We have to first initialize prefs to the defaults object using the standardUserDefaults method (making sure we retain it so it's available for future use):

prefs = [[NSUserDefaults standardUserDefaults] retain];

We then retrieve the stored array from prefs using objectForKey:. The key we use can be any string you want; we're going to use the string @"Addresses" here. So we send to prefs the message objectForKey:@"Addresses" and use the return value of that as the argument to initWithArray: (another NSMutableArray initializer method), and thus we have a stocked and ready to use mutable array:

records = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"Addresses"]];

Again we have the possibility that there may not exist a value for the key @"Addresses", in which case objectForKey: returns nil. This situation gets handled a bit differently than before. You see, when we used initWithContentsOfFile:, the method returned nil if the file could not be loaded, which provided a convenient means of testing whether our array had been initialized successfully. However, we're using the NSUserDefaults method objectForKey, in conjunction with the NSArray initializer initWithArray: to initialize records. If objectForKey cannot find a value for the specified key, nil is returned. Using nil as the argument to initWithArray: will initialize records to an empty array, rather than returning nil. So we can't use the test nil == records. Rather, we're going to invert our if-statement logic to say that if there is a key @" Addresses", initialize records from that preference, or else initialize records using init. To do this test in the if-statement we use the same NSUserDefaults method objectForKey:, but we're not interested in storing the return value, we're just interested in seeing if a value is returned. In code this looks like the following:

- (void)awakeFromNib
{
  prefs = [[NSUserDefaults standardUserDefaults] retain];

  if ( [prefs objectForKey:@"Adresses"] != nil ) {
    records = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"Addresses"]];
  } else {
    records = [[NSMutableArray alloc] init];
}

In general, to retrieve a default value from prefs, all we do is send it an objectForKey: message with the key corresponding to the particular preference we want to retrieve. However, NSUserDefaults defines several other methods such as arrayForKey:, boolForKey:, integerForKey:, and so on that allows us to be more specific as to what return type we expect.

The key difference here from objectForKey: is that objectForKey: will return nil if the specified key does not exist in the defaults database. arrayForKey:, on the other hand, goes a step further by returning nil if the object for the specified key is anything other than an array. The same is true for the other NSUserDefaults accessor methods and their respective return types.

Since we know for sure that the default we are using is going to be an array, we can use arrayForKey: rather than objectForKey:. This would change the relevant code to read in the second line of awakeFromNib:

records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];

The next step is to modify saveData to work with the defaults system. To store preferences to the database we use the NSUserDefaults method setObject: forKey:. We'll then get rid of the line in saveData [records writeToFile:recordsFile atomically:YES] and replace it with the code below:

- (void)saveData
{
    [prefs setObject:records forKey:@"Addresses"];
}

One last thing we have to do is change our application's bundle identifier, which is what NSUserDefaults uses to identify the file where a user's preferences are stored. To set this name click on the "Targets" tab in Project Builder and click on the target "AddressBook." When the view in the editor window changes to the target options, click on the "Applications" tab. In this view under Basic Information you will the field "Identifier." This is where you set the bundle identifier name.

This name can be anything you desire. Standard practice, however, is to use the naming scheme of Java packages to help avoid naming conflicts between different applications. For those not familiar with this naming scheme, it's basically an Internet address in reverse that contains information about the company's name and the name of the application. For example, preferences for the Dock are stored under the name com.apple.dock, Terminal is under com.apple.terminal and so on. Our application will follow this convention and take the name com.YourName/Company.AddressBook. My version uses as its identifier com.mikebeam.AddressBook.

Now, when the preferences are actually stored to the disk, they are written to the user's Library/Preferences/ directory with the file name equal to the bundle identifier with a plist extension. Thus, my preferences for AddressBook will be found at /Users/mike/Library/Preferences/com.mikebeam.AddressBook.plist.

Apple provides a useful tool to view and edit the contents of property list documents. It is the application PropertyListEditor, found under /Developer/Applications. It might be instructive to open the files created by AddressBook in the different implementations shown in this column and see how property lists are structured. If you want to get a look at the nitty-gritty XML tags, you can open plist files in any text editor, giving you even more insight into how property lists are structured.

dealloc

Before leaving for the day we're going to tie up another loose end in AddressBook by implementing the dealloc method. The dealloc method is defined in NSObject and this message is sent to classes that are about to be released, or de-allocated -- basically it serves the opposite purpose of alloc.

We override dealloc in our classes to prepare it for being released. One of the things commonly done with dealloc is to release any object-instance variables that we allocated (and subsequently own). For example, in Controller we have the instance variables records, recordsFile and prefs. So in our implementation of dealloc we would send release messages to each of these objects to counter each alloc or retain message we may have previously sent to them. So it's our responsibility to free up their memory when they are no longer needed, which is surely the case when the object that references them is being destroyed. With this, here is our dealloc to be added to Controller.m:

- (void)dealloc
{
    [self saveData];	// just to be sure that the 
                        // latest data is saved to disk

    [prefs synchronize];
    [prefs release];
    [records release];
    [recordsFile release];

    recordsFile = nil;
    records = nil;
    prefs = nil;

    [super dealloc];	// pass the ball to Controller's 
                        // superclass so it can do its own deallocation
}

What synchronize does is force the system to synchronize the defaults information contained in memory with what's stored on disk. Normally we don't manually use synchronize, as the system will do this automatically every so often. However, one time when we might want to force a synchronization is just before the application quits, as the system may not get a chance to do its auto-synchronization before the application closes. So that's what we're doing here.

Note that we didn't do anything with the outlet instance variables that we have. That's because we don't own those objects. We didn't assert ownership over them using alloc or retain, so we don't have to worry about them.

You may think dealloc is redundant in Controller since the memory occupied by all of the objects we released is freed when the app quits. However, say you want to use the Controller class here as part of some other application. This other application might not load Controller when the application launches as is done here, and the application might decide to release Controller sometime before the application quits.

In that case it would not be redundant to have a dealloc method, and by coding it right the first time, you save yourself some potentially time consuming work later. Besides, it's not that difficult or time consuming to implement dealloc like we've done here. So the moral of the story is to override the dealloc method whenever you have objects referenced by instance variables to clean up.

The End

I want to say a word about the two data-saving implementations we came up with today. Specifically, I want to address the question, "Which one is better? A separate file, or to store the data in the user defaults database?".

The answer to this is that user preferences are supposed to store non-critical data. That is, data that is not sorely missed if it is corrupted or lost. In our situation, it might be better to store the address book data in a separate file from user defaults, as that is something that you don't want to lose if you lose the preferences file.

But I guess one file is no safer than another file, so maybe the solution is to use both, using user defaults as the primary store house and then writing a separate file for backup. In awakeFromNib, if initializing records from prefs fails, then we can go to the backup file; if that fails, then we just initialize records to an empty array. Something like this:

if ( [prefs arrayForKey:@"Addresses"] != nil ) {
  records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
  } else {
    records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
    if ( nil == records )
      records = [[NSMutableArray alloc] init];
  }

The project folder for this column can be downloaded here. This downloadable version of Address Book is set up to store data in both user defaults and a separate file as a backup, like we just mentioned. Additionally, I've shown how to set up the table data source methods to allow you to directly modify the table data from within the table that I briefly mentioned in the previous column.

With the basics of using NSUserDefaults under our belt, next time we'll more fully leverage the preferences system, as well as learn how to work with multiple windows in an application (i.e. a Preferences window). See you next time!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.