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


Programming With Cocoa A Look Inside Address Book

by Michael Beam, O'Reilly Mac OS X Conference Speaker
08/27/2002

A Look Inside Address Book

Now that Jaguar is out, let's take a break from our recent series of graphics columns and look into one of the many new features of Mac OS X, version 10.2 that we Cocoa developers might be interested in: the AddressBook framework.

Prior to Jaguar, the Address Book application, and the collection of contacts contained within it, was an island of information that only a few privileged applications were able to access (Mail, in particular). However, with Jaguar, Apple has created a system-wide database of a user's contacts, and has provided an Objective-C API for accessing that database. This makes it possible for any number of third-party applications to not only access the contents of the address book database, but to perform any action on the contacts that may be done in Address Book, and more ....

Note: In this column when I say Address Book, with a space, I am referring to the application. AddressBook without a space refers to the framework, and finally, address book refers to the database.

If you're developing a Mac application that uses information about people, then I can't recommend highly enough integrating AddressBook into your software. This will prevent your application from being isolated from a feature that's becoming a core component of Mac OS X. As you'll see in this column, using the AddressBook framework is likely an easier task than implementing your own data model and your system of managing contacts. You'll also see how the database is extensible so that it can contain information specific to your application, without polluting other applications' use of the database.

As an example of how AddressBook can be integrated into an application, let's take a look at how iChat does it. If you open up iChat, you will see in your buddy list all of your AIM buddies. It's possible to assign information to a buddy, such as a person's real name, email address, and more. Now, rather than forcing you to enter this information by hand, iChat provides a button that pops up a dialog with a list of all your contacts in the AddressBook database. From this list you can select who you want to associate with the selected screen name. The information can then be synchronized in both directions, that is, Address Book will update its information about the contact to include the selected screen name, and iChat will display the person's real name rather than the screen name.

If you're not a fan of AIM, never fear. As you'll soon see, AddressBook provides built-in fields for not only AIM screen names, but also those for MSN, Jabber, and Yahoo. Thus, it is possible to achieve the same level of integration with AddressBook present in iChat in other applications such as Proteus, Fire, Adium, and MSN Messenger (if the developers choose to do so).

So, that's a quick look at where AddressBook can take us, feature-wise. Now let's now take a look to see how it's organized and how we can work with it all.

The Lay of the Land

The AddressBook framework stores information in a property list file for each user. Here we're already on familiar ground, as it is the same format for user defaults as well as data structures like arrays, dictionaries, strings, and numbers. This is, however, of trivial importance. All we care about is how to interact with the database.

The entry point to the system is through the class ABAddressBook, which we instantiate using the class method sharedAddressBook. From here we can access all of the address book records and also create new ones.

Records in the address book are instances of the class ABPerson or ABGroup, both of which inherit from the class ABRecord. ABPerson represents a single person and contains properties identifying information about that person, such as his email address, birthday, name, phone numbers, addresses, and more.

A group, on the other hand, is a collection of people and other groups. An ABGroup contains any number of people or additional groups, and carries only one property beyond those provided by ABRecord--the group name.

To access the contents of the address book, we have several options. We can make ABAddressBook return an array of all the people or all the groups it knows about by invoking the method people and groups, respectively. It is also possible to search the address book for a specific record or records matching some search criteria. Queries are created using the class ABSearchElement, which is then passed to the ABAddressBook instance to get the results of the search.

Download the application files for this article here.

To kick the tires of this framework we're going to go simple and code in a basic foundation tool. All of our output information will be displayed in the standard output in the run tab using NSLog, and the goal here is to just play around with the framework. We won't end up with an application at the conclusion, as we usually do.

Since the AddressBook framework is new to Jaguar, you have to have 10.2 installed to run these examples. To start out, create a new project from the File menu that is now a Foundation Tool. Now we have to add the AddressBook framework to the project and import the headers into our main source file. From the Project menu select Add Frameworks ... ; navigate to the /System/Library/Frameworks directory; and select AddressBook.framework. (This will only be present in Mac OS X 10.2. Additionally, you must have the most recent developer tools installed, or else you will get compilation errors.)

Be sure to add to the source file main.m the import statement:

#import <AddressBook/AddressBook.h>

To work with the framework, it's best to have several contacts in your address book. If you don't, then go ahead and open up Address Book and add some people to your address book and create a group or two if none exist yet.

Starting out with our Foundation tool all we have is a main function that creates an autorelease pool, does the whole "Hello, World" job, destroys the autorelease pool, then returns. The printf statement will be taken out and in its place we will put all of the code we wish to experiment with.

The first thing we want to do is instantiate ABAddressBook:

int main (int argc, const char *argv[]) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    ABAddressBook *book = [ABAddressBook sharedAddressBook];
    
    [pool release];
    return 0;
}

All of our test code from now on will go between where we instantiate ABAddressBook and where we release pool. With this in place let's work our way through the framework and see what we learn.

Records

A record is the fundamental collection of information in the AddressBook. Records can either be people, ABPerson; or groups, ABGroup. ABRecord is itself an abstract class, and is thus never instantiated. Records contain properties and values, which is similar to NSDictionary's use of keys and objects.

An array of all of the people in an address book is obtained by sending a people message to our ABAddressBook object. Similarly, we can send a groups message to the address book object to get an array containing the groups in the address book.

NSArray *everyone = [book people];
NSArray *everygroup = [book groups];

NSLog( [everyone description] );
NSLog( [everygroup description] );

This will print out the property list representation of the arrays everyone, and everyoneElse, which are respectively the top-level groups and people in the address book. The arrays actually contain ABPerson and ABGroup objects, one for each record. Using the people and groups methods is the quick way into the address book, useful if you want to present the user with a list of available contacts, such as is done in iChat.

A special kind of record in the address book is the record that identifies the current user, or "Me". This record is retrieved from the ABAddressBook object by sending it a me message. It is also possible to change "me" by invoking the method setMe:.

ABPerson *me = [book me];
NSLog( [me description] );

New records are created by instantiating either ABPerson or ABGroup, which is done using alloc and init. When a new record is created, it's given a unique id, which is stored in the kABUIDProperty defined in ABRecord. Once you have a new record object, you can add it to the address book using the ABAddressBook method addRecord:. If you want to remove a record, you may do so by sending a removeRecord: message to the address book object.

Related Reading

Building Cocoa Applications: A Step by Step Guide
By Simson Garfinkel, Michael Mahoney

Changes to the address book, including the addition or removal of records, are not made permanent (i.e. not written to file) until the save method of ABAddressBook is invoked. You can inquire into the existence of unsaved changes using the method hasUnsavedChanges.

Properties and Values

With the basics of working with records under our belt, it's time to learn how to manipulate the information contained within the record, which is why we're here after all. The first thing one would want to do is determine what's in the record. Property values are accessed using the NSDictionary-inspired method of ABRecord valueForProperty:. This method takes an NSString property name, and returns the value for that property. To set the value of a property we use the method setValue:forProperty:.

AddressBook defines the following list of NSString constants that identify the properties that a person may take on:

The list above applies only to ABPerson type records. Note that the last six properties in the list above are not supported in the Address Book user interface -- so you can use them, but you won't see any evidence of their existence in Address Book. ABGroup type records have only one property: kABGroupNameProperty. Both people and groups have the following properties, that are part of ABRecord:

The values of properties may be any of the supported property list data types: strings, numbers, dictionaries, dates, and data. Additionally, a property value may be an ABMultiValue data type, which is how AddressBook stores multiple values for a single property. Multiple values will be discussed at greater length below. In the list of ABPerson properties, all of the instant message properties are multivalue, as are the email, address, and phone number properties. The remainder are dates or strings.

The example below demonstrates how we can work with properties.

NSArray *everyone = [book people];
ABPerson *someone = [everyone objectAtIndex:0];

NSLog( [someone valueForProperty:kABFirstNameProperty] );
NSLog( [someone valueForProperty:kABLastNamePropertu] );

[someone setValue:@"Pete" forProperty:kABFirstNameProperty];

if ( [book hasUnsavedChanges] ) 
    [book save];

Multi-value Properties

Many property values in the AddressBook are typed as ABMultiValue, which is an object that stores multiple values for a single property. The idea behind ABMultiValue is that for many attributes of a person, such as phone numbers, email addresses, and address there exist multiple values. For example, most people have different addresses, phone numbers, and email addresses for their home and work. So rather than create several scattered properties for a work and home properties, AddressBook uses ABMultiValue which may contain an arbitrary number of values. This is by far a more flexible and cleaner solution.

An ABMultiValuee stores the multiple values for a property by index. Associated with each is a unique identifier, a string label, and a value. Generally, the label is some variant of home and work, such as kABAddressHomeLabel and kABAddressWorkLabel for the address property multi-value. However, it is possible to designate labels for additional values in the multi-value object beyond those for home and work (i.e. A summer vacation home address in addition to home and work). Additionally, while there are home and work labels specific to each multi-value property -- kABXXXXWorkLabel and kABXXXXHomeLabel -- AddressBook also defines the generic labels kABWorkLabel, kABHomeLabel, and kABOtherLabel which are equivalent to the property specific labels.

Every multi-value property has what is known as a primary identifier. The primary identifier identifies the sub value of the multi-value property that is most strongly associated with the person. For example, if you interact with a person purely on a professional basis, the primary identifier for that contact in your AddressBook would be for the work value of the contacts various properties. This identifier can be set in a ABMutableMultiValue using the instance method setPrimaryIdentifier:. Each multi-value property may have a different primary identifier.

Values are accessed in an ABMultiValue object by index using the method valueAtIndex:. If we wanted to access the primary value of the multi-value, we could do so by passing the primary identifier string -- obtained with the primaryIdentifier method -- to the method indexForIdentifier:. This returned index is then passed to valueAtIndex:.

To demonstrate how multi-value objects are used, let's take a closer look at the kABAddressBookProperty, a particularly interesting property since it contains NSDictionary objects rather than simple strings. The members of the address dictionary contain all of the pertinent address information such as street number and state. The following list is a list of the keys used to access values in the address dictionaries in the ABMultiValue object for kABAddressProperty.

Let's take a look at some examples of how we access multi-value properties:

ABMultiValue *addrs = [[book me] valueForProperty:kABAddressProperty];

int count = [addrs count];  // Determining the number of values

NSDictionary *addr = [addrs valueAtIndex:0];

NSString *primaryID = [addrs primaryIdentifier];
int idx = [addrs indexForIdentifier:primaryID]
NSDictionary *primaryAddr = [addrs valueAtIndex:idx];
NSString *primaryLabel = [addrs labelAtIndex:idx];

NSString *city = [primaryAddr objectForKey:kABAddressCityKey];

Defining New Properties

The AddressBook database is extensible, that is, an application can create additional properties for ABPerson objects and store information in those properties. Adding new properties will not disturb the way other applications interact with the Address Book -- what they don't know won't hurt them.

To add new properties we first create an NSDictionary whose keys are the property names, like kABFirstNameProperty, and the object for that key is the property type. AddressBook supports the following property types:

Since property names must be unique, it is best to adopt some naming convention such as is done with preference files: Java-style package naming. Once we have assembled our dictionary of property names and types, we pass it as a parameter to the ABPerson class method addPropertiesAndTypes:. Here is an example of this:

ABPerson *me = [book me];

NSMutableDictionary *new = [NSMutableDictionary dictionary]
[new setObject:@"kABStringProperty" forKey:@"com.myCompany.collegeProperty"];
[new setObject:@"kABDateProperty" forKey:@"com.myCompany.graduationDate];

[me setValue:@"The University of Texas"     
            forProperty::@"com.myCompany.collegeProperty"];

Because addPropertiesAndTypes: is a class method, it makes the property available for every person in the address book. Because of this, you should be judicious when adding new properties to make sure that it is applicable to a majority of the people in the address book. In other words, don't add address book properties if they only apply to one person in the database. For that, NSUserDefaults is a better solution.

Adding properties using addPropertiesAndTypes: has no effect if the property exists already with the specified type. Additionally, once a property has been added, is accessible just as any property. There is no need to add the property before attempting to access the value, or anything like that. You only need to add it the first time.

If you would like to remove a custom property completely, then you can do so using the ABPerson method removeProperties:. This method takes an NSArray containing all of the property names of the properties you would like removed. It returns the number of properties that were successfully removed.

Searching

A database isn't worth much without a way to search the contents. In address book, the class ABSearchElement is how we create queries with any range of complexity. A search element contains a simple search, like search the address book for all records with the first name Mike, or search for all records with birth dates occurring before the turn of the millennium.

Two or more ABSearchElement objects may be combined into a single composite search elements using either an AND operator or an OR operator. By combining search elements in this way it is possible to build quite complex queries.

Instances of ABSearchElement are created using the ABPerson or ABGroup class method searchElementForProperty:label:key:value:comparison:. Whether we invoke this method in the ABPerson class object or the ABGroup class object depends on whether we want to perform a search for people, or a search for groups. This method takes the following parameters:

The last parameter, comparison:, tells the search element how to determine a matching value. There are ten possible constants to pass here: kABEqual, kABEqualCaseInsensitive, kABLessThan, kABLessThanOrEqual, kABGreaterThan, kABGreaterThanOrEqual, kABContainsSubStringCaseInsensitive, kABPrefixMatch, kABPrefixMatchCaseInsensitive.

To create a composite, or conjunction, search we use the ABSearchElement class method searchElementForConjunction:children:. The first parameter tells the method how the child search elements will be combined. The possible values are kABAndSearch and kABOrSearch. The children: parameter takes an array of ABSearchElements that are to be combined in the conjunction search. Children search elements may be simple searches, or conjunction searches themselves.

The following is an example of searching the address book using search elements:

ABSearchElement *se;
NSArray *results;

se = [ABPerson searchElementForProperty:kABAddressProperty
                        label:kABAddressHomeLabel
                        key:kABAddressStateKey
                        value:@"Texas"
                        comparison:kABEqual];
results = [book recordsMatchingSearchElement:se];  
NSLog( [results description] );

As you can see above, the method recordsMatchingSearchElement: returns an NSArray containing all of the people in the address book whose home address is in Texas.

Performing a conjunction search is done similarly:

ABSearchElement *se1, *se2, *cse;
NSArray *children;

se1 = [ABPerson searchElementForProperty:kABAddressProperty
                        label:kABAddressHomeLabel
                        key:kABAddressStateKey
                        value:@"Texas"
                        comparison:kABEqual];
se2 = [ABPerson searchElementForProperty:kABBirthdayProperty
                        label:nil
                        key:nil
                        value:[NSDate dateWithTimeIntervalSinceReferenceDate:0]
                        comparison:kABLessThan];

children = [NSArray arrayWithObjects:se1, se2, nil];

cse = [ABSearchElement searchElementForConjunction:kABAndSearch         
                        children:children];

results = [book recordsMatchingSearchElement:cse];
NSLog( [results description] );

The composite search element cse will search for all people in the address book database who not only live in Texas, but who were born before the turn of the millennium, which we specify by creating a date that is zero seconds past the reference date (the absolute reference date used by NSDate is 12:00 AM, January 1, 2001 GMT).

Odds and Ends

To tie up loose ends, let's talk a bit about assigning images to an ABRecord, as well as how we can import and export vCards to and from the AddressBook database.

Creating a vCard is easily accomplished using the ABRecord method vCardRepresentation. This method returns an NSData object whose data is formatted in the vCard format. We can then write this data to disk, which can be read by any number of applications that recognize the vCard format. Going the other way we can initialize an ABRecord object with vCard data using the method initWithVCardRepresentation:. This method takes a parameter an NSData object, which was likely created by reading the contents of a vCard file on disk created by another application.

Finally, to associate an image with a person in the AddressBook we use the methods setTIFFImageData: and TIFFImageData to set and get the person's picture. These methods work with NSData objects whose data is formatted as a TIFF image. This interfaces well with the NSImage methods TIFFRepresentation, which returns an TIFF formatted NSData object, and initWithData:, which initializes an NSImage object with image data.

The End

So, that was a whirlwind tour of the AddressBook framework. It's not really my style to leave you empty-handed at the end of a column, so I've prepared a version of our old-school application Address Book that we worked on about a year ago while learning about NSTableView. This version has been revamped to display the contents of the user's address book in a table, and allow the user to edit records. It's basically the same thing we had before, except the data model is based on ABAddressBook rather than on NSArray and NSDictionary. This is an excellent example of the utility of MVC since the model was separated from the view and insulated by the controller, so it was relatively pain-free to change.

So, happy Jaguar release, and next time we'll get back to those plug-ins.

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.