AddThis Social Bookmark Button

Print
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.


Pages: 1, 2, 3

Next Pagearrow