MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Mac OS X's Preferences System (and More!)

08/24/2001

Little by little in this series we're expanding our knowledge of Cocoa, and continually adding to our repertoire of programming tools. Today, we carry on with a continuation of the previous article by enhancing our Address Book application.

This application is an address book only by virtue of the names we give the data fields and the table columns. The application logic, data structures and GUI are common to a whole class of applications of this type, and you can and will be able to use many of the elements of this application in your software. In that spirit let's continue.

Today, we will tie up some loose ends from the previous column. This will involve a discussion of several ways we can save the address book data between launches, and then go on to touch upon some memory management issues. In this process, we'll also learn about Mac OS X's preferences system.

Saving Data From Arrays and Dictionaries

Recall that at the close of the previous column our address book had no way to save and retrieve data between launches -- a serious shortcoming in any application. One way we could have done this is by making our program a document-based application from the outset and overriding the file-saving and loading methods that we've learned about.

We're going to take a different route than this. Rather than having the user worry about saving and opening address book files between sessions, we'll have the application automatically load the data at each launch, and save changes to the data whenever it is modified.

Loading Data

The first thing we need to do to set up automatic data saving and loading is change the way our data structure, i.e., records, is initialized in awakeFromNib. When we finished AddressBook at the end of the previous column we had the following single line of code in awakeFromNib:

- (void)awakeFromNib
{
    records = [[NSMutableArray alloc] init];
}

Which simply initialized records as an empty mutable array. NSMutableArray has another init... method (inherited from NSArray) that allows us to initialize a newly allocated array with the contents of a text file, which is exactly the type of thing that will prove useful for us. Of course, the text file from which we initialize records must be formatted in a particular way, which is a type of XML document known as a property list. The NSArray methods we use to save the contents of records to disk will take care of the formatting details, so you don't have to. Nevertheless, in a moment we'll briefly say a few words about property lists.

The method we're going to use to load previously stored data is named initWithContentsOfFile:, which takes an NSString argument that is the path to the file we wish to initialize the receiver with. So let's change our allocation and initialization code to the following, simply replacing init with initWithContentsOfFile:

- (void)awakeFromNib
{
records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
}

The argument variable recordsFile is as of yet undefined, but this will be the string that contains the path to our data file. Let's now go into the interface file, Controller.h, and add an instance variable declaration for recordsFile so we can access the path from any method of Controller. This is the line that needs to be added to the instance variable declaration block of Controller.h:

NSString *recordsFile;

Now we need to initialize recordsFile to the path of the data file for our address book. This too is done in awakeFromNib, directly before the line that initializes records. Here is awakeFromNib after adding the code to set up recordsFile:

- (void)awakeFromNib
{
   recordsFile = [NSString stringWithString:@"~/Library/Preferences/AddressBookData.plist"];
   recordsFile = [recordsFile stringByExpandingTildeInPath];
   [recordsFile retain];

   records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
}

In the first line we initialize recordsFile to the string @"~/Library/Preferences/AddressBookData.plist" using the convenience constructor stringWithString:. By using a tilde (~)for this path, we effectively set up our application to store different data files for each user of this application.

Comment on this articleBy now you should have a fully functional address book application. What have you learned and what road blocks have you encountered?
Post your comments

Also in Programming With Cocoa:

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

A convenience constructor is used here in place of the more straightforward @"..." syntax so that this first string pointed to by recordsFile is autoreleased, rather than uselessly taking up memory. In the next line we immediately reassign recordsFile to the object returned by stringByExpandingTildeInPath, which is the real string we want to use.

Remember that methods that call for a path don't know how to handle paths relative to tildes, so we have to invoke this method to change the relative path into an absolute path containing the appropriate user's home directory.

At this point we would have two string objects floating around. The one with the tilde is somewhere in memory where we can't get it because we reassigned recordsFile, but that's OK since it's set to be autoreleased. The other one is the absolute path we last created, and it too is set for autorelease (under the assumption that method return objects set for autorelease), but that is not OK since we need this path string to stick around. We easily remedy that situation by sending recordsFile a retain, and we're set to go.

The implementation of awakeFromNib that we have just written has a non-trivial shortcoming in the assumption that recordsFile indicates a file that exists and can be used to initialize an array. What if the file doesn't exist because the user hasn't run this application yet, or maybe it was deleted somehow? If the array doesn't get allocated and initialized the application will crash the first time we send a message to records. Clearly this is undesirable behavior. To prevent this from happening we have to code in a contingency plan for initializing records.

Our contingency plan relies on the return value of initWithContentsOfFile:. That is, if the file indicated by recordsFile does not exist or it cannot be properly parsed and loaded, then nil is returned. We can then check to see whether records is nil, and in the situation that initWithContentsOfFile: fails, we can initialize records with an empty array. The code for this might look like the following:

- (void)awakeFromNib
{
  recordsFile = @"~/Library/Preferences/AddressBookData.plist";
  recordsFile = [recordsFile stringByExpandingTildeInPath];
    [recordsFile retain];
  records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
  if ( nil == records ) {
    records = [[NSMutableArray alloc] init];
  }
}

Notice how we wrote the conditional test as nil == records, rather than records == nil. This comes from a tip I received on Apple's cocoa-dev mailing list (you can join at http://lists.apple.com), and the idea is that if you intend to write records == nil (comparing the two objects), but accidentally write records = nil (assigning nil to records), then you won't get any compiler feedback since the second construct is perfectly legal.

However, if you intend to write nil == records, but accidentally write nil = records, then you will get a compiler warning informing you of your mistake. Something like this is really a matter of personal coding style, but I thought I would mention it anyway.

And this completes our implementation of loading data from a file into records. On to saving data.


Saving Data

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

Now that we have a way to load previously saved data into records, we need to implement its counterpart: a way to save the data. This is easily accomplished with the method writeToFile:atomically:. The first argument to this method is again recordsFile, and the second argument takes a BOOL value -- YES or NO.

If atomically is NO, then the data will be written directly to the file at the indicated path. If atomically is set to YES, then the method will write the contents of the message receiver to a temporary file, and only when it determines the write to be a success will the actual data file will be replaced by the temporary file. This process will prevent the data file from being corrupted in the event of any system failure or crash, so it's almost always a good idea to use YES as the atomically argument. With the following line then, we can save the contents of records to the file at recordsFile:

[records writeToFile:recordsFile atomically:YES];

Ideally we would like to save the contents of records to disk whenever they are modified. By doing this we don't have to worry about losing unsaved changes if something unexpectedly goes wrong with the application. So we would then want to invoke writeToFile:atomically: in addRecord:, deleteRecord:, and insertRecord: (if you implemented the NSTableDataSource method tableView:setObjectValue:forTableColumn:row: that I mentioned in the previous column, then you would invoke it there, too).

Rather than scattering this line around in each method that modifies records, we will create a new method in Controller called saveData, and call saveData in each of the aforementioned modifying methods. The definition of saveData is shown below:

- (void)saveData
{
    [records writeToFile:recordsFile atomically:YES];
}

Factoring out the data-saving code into saveData has the desirable benefit of making our code more maintainable. If we decide to change the way data is saved, then we only have to change it in one place.

Now all we have to do is add the line of code [self saveData] to each of the methods deleteRecord, addRecord and insertRecord right around where we do [tableView reloadData]. The method addRecord, for example, will now look like:

- (IBAction)addRecord:(id)sender
{
    [records addObject:[self createRecord]];
    [tableView reloadData];
    [self saveData];
}

With these simple pieces of code in place, your application is now capable of saving and loading the same set of data between different sessions. Now I want to say a word about property list documents, and then we'll go on to discuss Mac OS X's preferences system.

Property Lists

A property list is a type of XML (eXtensible Markup Language) that is used extensively by Mac OS X to store software configuration information such as user defaults. The contents of a property list is an XML structured textual representation of the memory data of runtime objects of classes such as NSDictionary, NSString and NSArray. Property lists are then able to preserve the structure and contents of our data structure at each level-array, dictionary and string. I'm not going to get into a discussion about XML here, because the topic is just too huge for this column. For a good summary of XML, see WC3's XML in 10 points or visit O'Reilly's XML.com.

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.

Copyright © 2009 O'Reilly Media, Inc.