oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

Adding a Preferences Window to Your Application
Pages: 1, 2, 3, 4, 5, 6, 7

Consider that rather than making six calls to addInitialColumnForIdentifier, we create an array of identifier strings; call this array identifiers. Then we could get create an enumerator for that array, use the enumerator in a while loop, and within that loop send an addInitialColumnForIdentifier message to self. Here, look at the code that we could have:

NSArray *identifiers = [NSArray arrayWithObjects:@"First Name", @"Last Name", @"Email", @"Home Phone", @"Work Phone", @"Mobile Phone", nil];

id identifier;
NSEnumerator *e = [identifiers objectEnumerator];

while ( (identifier = [e nextObject]) ) {
  [self addInitialColumnForIdentifier:identifier];

Following our trend of creating new methods to encapsulate new logic, let's make a method similar to addInitialColumnForIdentifier: called addInitialColumnsForIdentifiers: whose argument is an array of identifier strings:

- (void)addInitialColumnsForIdentifiers:(NSArray *)identifiers
  id identifier;
  NSEnumerator *e = [identifiers objectEnumerator];

  while ( (identifier = [e nextObject]) ) {
    [self addInitialColumnForIdentifier:identifier];

and then in awakeForNib we need only write:

NSArray *identifiers; // At the beginning of awakeFromNib

identifiers = [NSArray arrayWithObjects:@"First Name", @"Last Name", @"Email", @"Home Phone", @"Work Phone", @"Mobile Phone", nil];
[self addInitialColumnsForIdentifiers:identifiers];

And we have abstracted the decision of which columns we want to include a bit further away from the code that fills tableColumns, and I say that makes things more flexible, and the possibilities more interesting.

For example, no one said we had to statically create the identifiers array like we did above. We could instead initialize identifiers from a file. Yes ... that would make our application quite flexible indeed. Think about it. If we implemented our code in such a way, nowhere in our code would we see what the column identifiers are -- rather, they are defined in an external file. The decision on what columns to create comes when the user runs the application, not when it is compiled


The clever developer could extend this concept a bit further to dynamically create the data entry form and the table column preference check boxes at run time based on the column identifiers contained in this external file. Then, the clever user could tailor this application to organize and display whatever data fields he wishes (since really it's just a matter of what we name the data fields and table columns, right?) by mucking around with the file that contains the identifier declarations.

Let's change this code one more time to load the identifiers array from a file, using the class NSBundle.


In the last column, we learned how we could initialize arrays from arbitrary files. Now I want to expand on this concept and tell you how you can initialize arrays from files that are resources within an application bundle. Bundles are one of the wonderful new things about Mac OS X. Essentially a bundle is a directory that contains all of the resources an application needs to run, including special images, and sounds, the nib files, the actual executable, and any number of configuration files. In the Finder, a bundle appears as an executable for the application. It is the .app extension of an application bundle that the finder recognizes so that the contents of the bundle should not be displayed to the user. This keeps your system cleaner and more organized, and reduces the risk that a mischievous user might inadvertently delete some critical application file.

Cocoa provides an interface in the AppKit class NSBundle for accessing resources contained within an application's bundle. The array identifiers will be initialized using initWithContentsOfFile: in the same way we learned to do so previously. Rather than declaring a static path, use NSBundle's pathForResource:ofType: which locates the indicated resource in the application bundle and returns the absolute path. pathForResource:ofType: takes two arguments: the name of the resource, and the file extension, or type of the resource. This file will be named Identifiers.plist.

We know the name of the resource we wish to access -- Identifiers -- as well as the file type of this resource -- plist. We need to have an NSBundle instance to send messages to, which is created using the class method mainBundle. Let's see how this all looks in code:

NSBundle *bundle;	// These first three lines go
                    // at the beginning of awakeFromNib
NSString *path;
NSArray *identifiers;

bundle = [NSBundle mainBundle];
path = [bundle pathForResource:@"Identifiers"
identifiers = [[NSArray alloc] 
[self addInitialColumnsForIdentifiers:identifiers];

And that's how we can access resources contained within a bundle. Obviously there is much more we can do with NSBundle, but I just wanted to briefly introduce you to the possibilities. For the complete capabilities of NSBundle refer to its class documentation.

One last important thing we have to do to wrap up the bundle implementation of step one of our strategy is create the actual resource from which the array identifiers will be initialized from. This is easily done by creating a new empty file in your project; call this file Identifiers.plist; it will automatically be placed in your "Resources" group, and it will automatically be placed in the application bundle when you compile. The contents of this file will be the following:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
  <string>First Name</string>
  <string>Last Name</string>
  <string>Home Phone</string>
  <string>Work Phone</string>
  <string>Mobile Phone</string>

This is just a standard XML property list representation for an array of strings, that is functionally equivalent to the static array initialization line we had using initWithObjects:.

At this point awakeFromNib should look like the code below:

- (void)awakeFromNib
  NSBundle *bundle;
  NSString *path;
  NSArray *identifiers;

  prefs = [[NSUserDefaults standardUserDefaults] retain];
  tableColumns = [[NSMutableDictionary alloc] init];

  recordsFile = [NSString stringWithString:@"~/Library/Preferences/AddressBookData.plist"];
  recordsFile = [[recordsFile stringByExpandingTildeInPath] retain];
  if ( [prefs arrayForKey:@"Addresses"] != nil )
    records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
  else if ( [[NSFileManager defaultManager] fileExistsAtPath:recordsFile] == YES )
    records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
    records = [[NSMutableArray alloc] init];
  // Load the identifiers array from the bundle resources
  bundle = [NSBundle mainBundle];
  path = [bundle pathForResource:@"Identifiers" ofType:@"plist"];
  identifiers = [[NSArray alloc] initWithContentsOfFile:path];

  [self addInitialColumnsForIdentifiers:identifiers];

This completes the first part of our strategy for the coding, onto part 2!

Pages: 1, 2, 3, 4, 5, 6, 7

Next Pagearrow