|
| | | | | | |
| | | |
| |
|
OS X & Cocoa Writings by Andrew C. Stone ©1995-2003 Andrew C. Stone |
|
|
|
|
Little Black Book
Integrating the AddressBook framework into your Application
©2002 Andrew C. Stone. All Rights Reserved
The advent of Jaguar brought a raft of new cool features, including the AddressBook Framework which allows developers access to the system-wide database of contacts and all their various phone numbers, addresses and emails. The end user application Address Book from Apple provides a front end to this database which lets you add, delete, edit and search for people and groups. In this article, you will learn the basics of the Address Book framework, how to create an interface to display groups and people, and how to build a single binary that takes advantage of the AddressBook framework, but also runs on older versions of Mac OS X 10.1, even though this framework does not exist on those systems! Both Stone Design's Create® and TimeEqualsMoney™ can now accept drag & drop VCARDs and show a Person chooser: |
| |
|
|
This simple interface displays the groups and members of a user's Address Book |
|
VCARDs are widely used in people's signature files in Email and generally appear as a "Rolladex"-like icon. VCARD is also the format used by Address Book when you drag a person into another application. Because the framework knows how to create a "Person" from the drag pasteboard data, you can easily add the ability to accept VCARDs via drag and drop. The person chooser, although we run it modally, is set to receive and display updates if the Address Book database is modified by some other application using the NSNotificationCenter.
The AddressBook framework is quite simple, and the top level object is the ABAddressBook. You gain access to the addressBook with:
ABAddressBook *book = [ABAddressBook sharedAddressBook];
First read over ABRecord.h as it's the superclass of two important classes: ABGroup and ABPerson. Study ABGlobals.h for the set of "keys" that can be used to extract data from an ABPerson or ABGroup with valueForProperty:. You can quickly grab all the people in the book with:
NSArray *people = [book people];
and the groups of people with:
NSArray *groups = [book groups];
and the list of ABPersons of a group with:
NSArray *people = [group members];
Once you have a person in your hand, you use valueForProperty: to get at the actual strings. For example, here's how you might build a simple full name:
#define IS_NULL(s) (!s || [s isEqualToString:@""])
- (NSString *)fullName:(ABPerson *)person {
id first = [person valueForProperty:kABFirstNameProperty];
id last = [person valueForProperty:kABLastNameProperty];
if (IS_NULL(first)) return last;
if (IS_NULL(last)) return first;
return [NSString stringWithFormat:@"%@ %@", first,last];
}
To make this even better, you probably want to look at these other keys and if not null, add them to the name. Currently, they are not supported in this release of Address Book:
// ----- Person Properties not Currently supported in the AddressBook UI
extern NSString * const kABMiddleNameProperty; // string
extern NSString * const kABTitleProperty; // string "Sir" "Duke" "General" "Lord"
Some properties are likely to have multiple values. For example, the phone number property, kABPhoneProperty, may include numbers for home, work, cell, pager, and others. For properties like these, the framework returns an ABMultiValue object, which consists of multiple values and labels. To retrieve a particular value, you need to walk over the values and check their labels. For example, this code extracts a main or work phone number:
obj = [person valueForProperty:kABPhoneProperty];
if (obj) {
int count = [obj count];
if (count > 1) {
unsigned i;
for (i = 0; i < count; i++) {
NSString *label = [obj labelAtIndex:i];
if ([label isEqualToString:kABPhoneMainLabel] ||
[label isEqualToString:kABPhoneWorkLabel])
return [obj valueAtIndex:i];
}
} else return [obj valueAtIndex:0]; // the one and only
}
Another key that returns the multi-value object is the kABAddressProperty. This actually returns a dictionary. You can determine what kind of something a key will return with:
- (ABPropertyType)propertyType;
// Type of this multivalue (kABMultiXXXXProperty)
// Returns kABErrorInProperty in this multi-value is empty or not all values have
// the same type.
These are defined in ABTypedefs.h and can help you find out what to expect when grabbing a valueAtIndex:!
Here's how you might build a string that contains a user's work address:
- (NSString *)mailAddressForPerson:(ABPerson *)person {
id obj = [person valueForProperty:kABAddressProperty];
NSMutableString *s = [NSMutableString string];
// re-use the function from above:
[s appendString:[self fullName:person]];
if (obj) {
NSDictionary *dic;
[s appendString:@"\n"];
int count = [obj count];
if (count > 1) {
// find the right one (we'll use Work if available)
unsigned i;
for (i = 0; i < count; i++) {
if ([[obj labelAtIndex:i] isEqualToString:kABAddressWorkLabel])
dic = [obj valueAtIndex:i];
}
} else dic = [obj valueAtIndex:0];
// we should have the work dict now:
obj = [dic objectForKey:kABAddressStreetKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\n"];
}
obj = [dic objectForKey:kABAddressCityKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@" "];
}
obj = [dic objectForKey:kABAddressStateKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\t"];
}
obj = [dic objectForKey:kABAddressZIPKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\n"];
}
obj = [dic objectForKey:kABAddressCountryKey];
if (NOT_NULL(obj)) {
[s appendString:obj];
}
return s;
}
Presenting The Address Book
It's simple to grab the groups and people - but what's a good way to present them? I've fallen in love with NSTableView and it's subclass, NSOutlineView, because they allow a clean and easy way to display data. AddressBookLookUp is a self-contained NSWindowController subclass that returns the chosen person when you run it with:
ABPerson *personChosen = [[AddressBookLookup sharedInstance] choosePersonModally];
The one odd thing about this class is the extra "dict" variable, which caches the sorted members of each group. Because I didn't find a way to get back sorted People from ABAddressBook - I had to roll my own sort mechanism. Because of the way that NSOutlineView does lazy loading, we have to cache our sorts, otherwise, it would attempt to resort every time it tries to display each member!
So, our workaround is to create a mutable dictionary, and store the sorted array of people for each group by using the group's name as the key. We'll sort on the last name key by creating a simple function:
int memberSort(id item1, id item2, void *context)
{
return [[item1 valueForProperty:kABLastNameProperty] caseInsensitiveCompare:[item2 valueForProperty:kABLastNameProperty]];
}
And then we'll build a cover to see if its been cached or not, and get it if needed:
- (NSArray *)sortedMembers:(ABGroup *)group {
id obj = [dict objectForKey:[group valueForProperty:kABGroupNameProperty]];
if (obj) return obj;
else {
NSMutableArray *a = [NSMutableArray arrayWithArray:[group members]];
[a sortUsingFunction:memberSort context:NULL];
[dict setObject:a forKey:[group valueForProperty:kABGroupNameProperty]];
return a;
}
}
/* AddressBook */
#import <Cocoa/Cocoa.h>
@interface AddressBookLookup : NSWindowController
{
IBOutlet id okButton;
IBOutlet id outlineView;
NSMutableDictionary *dict;
}
- (IBAction)validateOKButtonAction:(id)sender;
- (ABPerson *)choosePersonModally;
+ (id)sharedInstance;
- (IBAction)cancel:(id)sender;
- (IBAction)ok:(id)sender;
@end
#import "../timeclock.h"
#import <AddressBook/AddressBook.h>
#import "AddressBookLookup.h"
@implementation AddressBookLookup
// if you run as a sheet, you'll want individual instances
// otherwise, you can use this one single instance:
static AddressBookLookup *shared = nil;
+ (id)sharedInstance {
if (!shared) {
shared = [[self allocWithZone:NULL] init];
}
return shared;
}
// this is clever, reusable code that relies on the NIB being named
// the same as this class, ie AddressLookup.nib:
- (id)init {
self = [self initWithWindowNibName:NSStringFromClass([self class])];
if (self) {
[self setWindowFrameAutosaveName:NSStringFromClass([self class])];
// we'll only reload data lazily when an external update occurs:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookUpdatedNotification:) name:kABDatabaseChangedExternallyNotification object:nil];
// we hold onto sorted lists of members:
dict = [[NSMutableDictionary dictionary] retain];
}
return self;
}
- (IBAction)cancel:(id)sender
{
[[self window] performSelector:@selector(orderOut:) withObject:nil afterDelay:0.1];
[NSApp abortModal];
}
- (IBAction)ok:sender
{
[[self window] performSelector:@selector(orderOut:) withObject:nil afterDelay:0.1];
[NSApp stopModal];
}
- (void)addressBookUpdatedNotification:(NSNotification *)notification {
// some external process has updated the AddressBook - let's throw away our cached sorted lists:
[dict release]; // from lasttime or nil
dict = [[NSMutableDictionary dictionary] retain];
[outlineView reloadData];
}
- (ABPerson *)choosePersonModally {
NSWindow *window;
window = [self window]; // this creates it if needed!
[self updateUI];
if ([NSApp runModalForWindow:window] == NSRunStoppedResponse)
return [self selectedPerson];
else return nil;
}
- (IBAction)validateOKButtonAction:(id)sender
{
[self updateUI];
}
// we sort on last name:
int memberSort(id item1, id item2, void *context)
{
return [[item1 valueForProperty:kABLastNameProperty] caseInsensitiveCompare:[item2 valueForProperty:kABLastNameProperty]];
}
- (NSArray *)sortedMembers:(ABGroup *)group {
id obj = [dict objectForKey:[group valueForProperty:kABGroupNameProperty]];
if (obj) return obj;
else {
NSMutableArray *a = [NSMutableArray arrayWithArray:[group members]];
[a sortUsingFunction:memberSort context:NULL];
[dict setObject:a forKey:[group valueForProperty:kABGroupNameProperty]];
return a;
}
}
- (NSArray *)groups {
ABAddressBook *sharedAddressBook = [ABAddressBook sharedAddressBook];
return [sharedAddressBook groups];
}
- (ABPerson *)selectedPerson {
int row = [outlineView selectedRow];
if (row > -1) { // only groups in col 0
ABPerson *node = [outlineView itemAtRow:row];
if ([node isKindOfClass:[ABPerson class]]) return node;
}
return nil;
}
- (void)appendString:(NSString *)s to:(NSMutableString *)string {
if (NOT_NULL(s)) {
[string appendString:s];
[string appendString:@" "];
}
}
- (NSString *)stringForPerson:(ABPerson *)person {
NSMutableString *s = [NSMutableString string];
[self appendString:[person valueForProperty:kABFirstNameProperty] to:s];
[self appendString:[person valueForProperty:kABLastNameProperty] to:s];
[self appendString:[person valueForProperty:kABOrganizationProperty] to:s];
return s;
}
- (void)updateUI {
// set the enabled of the OK button to whether a person is selected
[okButton setEnabled:[self selectedPerson] != nil];
}
//
// OutlineView Data source methods:
//
- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
[self updateUI];
}
- (int)totalInGroup:(ABGroup *)g {
return [[g members] count] + [[g subgroups] count];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item {
return NO;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(int)index
ofItem:(id)item {
/*
Implemented by the data-source delegate. Children of a given parent item are accessed sequentially. This method should return the child item at the specified index. If item is nil, this method should return the appropriate child item of the root object.
*/
if (item == nil) return [[self groups] objectAtIndex:index];
else {
int numSubgroups = [[item subgroups] count];
if (index < numSubgroups) return [[item subgroups] objectAtIndex:index];
return [[self sortedMembers:item] objectAtIndex:index - numSubgroups];
}
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
/*
Implemented by the data-source delegate. This method should return true if item can be expanded to display its children.
*/
if ([item isKindOfClass:[ABPerson class]]) return NO;
if ([item isKindOfClass:[ABGroup class]]) return [self totalInGroup:item] > 0;
return NO;
}
- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
/*
Implemented by the data-source delegate. This method returns the number of child items encompassed by item. If item is nil, this method should return the number of children for the top-level item.
*/
if (item == nil) return [[self groups] count];
else if ([item isKindOfClass:[ABGroup class]])
return [self totalInGroup:item];
else return [[item members] count];
}
- (id)outlineView:(NSOutlineView *)outlineview
objectValueForTableColumn:(NSTableColumn *)tableColumn
byItem:(id)item {
/*
Implemented by the data-source delegate. Returns the data object associated with the specified item. The item is located in the specified tableColumn of the view.
*/
if (item == nil) return @"People";
if ([item isKindOfClass:[ABGroup class]]) return
[item valueForProperty:kABGroupNameProperty];
if ([[outlineview tableColumns] indexOfObject:tableColumn] == 0)
return [item valueForProperty:kABLastNameProperty];
else return [self stringForPerson:item];
}
- (void)outlineView:(NSOutlineView *)outlineView
setObjectValue:(id)object
forTableColumn:(NSTableColumn *)tableColumn
byItem:(id)item {
/*
Implemented by the data-source delegate. Sets the data object for the specified item. The object parameter contains the data to be set. The item is located in the specified tableColumn of the view.
*/
}
@end
//
// AddressBookStubs.h
// StoneTime
//
// Created by Andrew Stone on Mon Nov 04 2002.
// Copyright (c) 2002 __MyCompanyName__. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface AddressBookStubs : NSObject {
}
- (void)loadPerson:(ABPerson *)person intoDocument:(id)doc;
- (void)loadPersonData:(NSData *)data intoDocument:(id)doc;
@end
//
// AddressBookStubs.m
// StoneTime
//
// Created by Andrew Stone on Mon Nov 04 2002.
// Copyright (c) 2002 __MyCompanyName__. All rights reserved.
//
#import "../timeclock.h"
#import <AddressBook/AddressBook.h>
#import "AddressBookStubs.h"
@implementation AddressBookStubs
static AddressBookStubs *dude = nil;
+ (id)sharedInstance {
if (!dude) dude = [[AddressBookStubs alloc] init];
return dude;
}
// AddressBook stuff
- (NSString *)fullName:(ABPerson *)person {
id first = [person valueForProperty:kABFirstNameProperty];
id last = [person valueForProperty:kABLastNameProperty];
if (IS_NULL(first)) return last;
if (IS_NULL(last)) return first;
return [NSString stringWithFormat:@"%@ %@", first,last];
}
- (void)loadPerson:(ABPerson *)person intoDocument:(id)doc {
id obj;
[doc setClientName:[self fullName:person]];
obj = [person valueForProperty:kABEmailProperty];
if (obj) {
NSString *email;
int count = [obj count];
if (count > 1) {
// find the right one (work?)
unsigned i;
for (i = 0; i < count; i++) {
if ([[obj labelAtIndex:i] isEqualToString:kABEmailWorkLabel])
email = [obj valueAtIndex:i];
}
} else email = [obj valueAtIndex:0];
if (NOT_NULL(email)) [doc setEmail:email];
}
obj = [person valueForProperty:kABPhoneProperty];
if (obj) {
NSString *phone = nil;
int count = [obj count];
if (count > 1) {
// find the right one (work?)
unsigned i;
for (i = 0; i < count; i++) {
NSString *label = [obj labelAtIndex:i];
if ([label isEqualToString:kABPhoneMainLabel] ||
[label isEqualToString:kABPhoneWorkLabel])
phone = [obj valueAtIndex:i];
}
}
if (IS_NULL(phone)) phone = [obj valueAtIndex:0];
if (NOT_NULL(phone)) [doc setPhone:phone];
}
obj = [person valueForProperty:kABAddressProperty];
if (obj) {
NSMutableString *s = [NSMutableString string];
NSDictionary *dic;
[s appendString:[self fullName:person]];
[s appendString:@"\n"];
int count = [obj count];
if (count > 1) {
// find the right one (work?)
unsigned i;
for (i = 0; i < count; i++) {
if ([[obj labelAtIndex:i] isEqualToString:kABAddressWorkLabel])
dic = [obj valueAtIndex:i];
}
} else dic = [obj valueAtIndex:0];
// we should have the work dict now:
obj = [dic objectForKey:kABAddressStreetKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\n"];
}
obj = [dic objectForKey:kABAddressCityKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@" "];
}
obj = [dic objectForKey:kABAddressStateKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\t"];
}
obj = [dic objectForKey:kABAddressZIPKey];
if (NOT_NULL(obj)) {
[s appendString:obj]; [s appendString:@"\n"];
}
obj = [dic objectForKey:kABAddressCountryKey];
if (NOT_NULL(obj)) {
[s appendString:obj];
}
if (NOT_NULL(s)) [doc setAddress:s];
}
}
- (void)loadPersonData:(NSData *)data intoDocument:(id)doc{
[self loadPerson:[[[ABPerson alloc] initWithVCardRepresentation:data]autorelease] intoDocument:(id)doc];
}
Beating DYLD to the punch
How can we build a single app that will run on both Jaguar and earlier systems that don't have the AddressBook framework?
Here's what happens if you link the AddressBook framework to your app and then run it on Mac OS X 10.1, assuming the app is named "addressFrameCrasher":
dyld: /Volumes/CC_Space/AA_TEST/addressFrameCrasher.app/Contents/MacOS/addressFrameCrasher can't open library: /System/Library/Frameworks/AddressBook.framework/Versions/A/AddressBook (No such file or directory, errno = 2)
And then the application "autoquits". So, the trick is to isolate all the code which references any AddressBook symbol into separate bundles: an interface bundle (AddressBookLookup) and a logic bundle that knows how to do something useful with an address (AddressBookStubs). Only these two bundles are linked against the AddressBook framework. At runtime, check if we're running Jaguar or higher and only then load the bundles linked against the AddressBook framework. Otherwise put up an alert to tell them to get Jaguar or perhaps remove or disable the menu item:
- (void)newFromAddressBookAction:(id)sender {
if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_1) {
/* On a 10.1 - 10.1.x system */
NSRunAlertPanel(@"Jaguar",@"You need Mac 10.2 Jaguar for this feature to work!",@"OK",NULL,NULL);
} else {
[[MyDocument sharedInstanceOfClassName:@"AddressBookLookup"] createNewWithAddressBook];
}
}
And here's how we isolate the bundles from the app - we just refer to the class a string and load it like this:
+ (id)instanceOfClassName:(NSString *)name shared:(BOOL)shared
{
id obj = nil;
NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:@"bundle"];
if (path) {
NSBundle *b = [[NSBundle allocWithZone:NULL] initWithPath:path];
if ((b != nil) && ([b principalClass] !=NULL)) {
if (shared) obj = [[b principalClass] sharedInstance];
else obj = [[[b principalClass] allocWithZone:NULL] init];
}
}
return obj;
}
+ (id)sharedInstanceOfClassName:(NSString *)name
{
return [self instanceOfClassName:name shared:YES];
}
+ (id)instanceOfClassName:(NSString *)name {
return [self instanceOfClassName:name shared:NO];
}
Adding bundles to your ProjectBuilder project is simple:
1. Project -> New Target... , "Bundle", and add it to your resources in your application Target
2. Add the files AddressBookStubs.h, AddressBookStubs.m
3. Add the Cocoa and AddressBook frameworks to "Frameworks and Libraries" in AddressBookStubs
4. Make your app's target depend on the AddressBookStubs target
5. Repeat steps 1-4 for AddressBookLookup.h, .m and .nib
Right when you thought you got it licked, boom! Another crasher - this time an undefined symbol:
/Volumes/CC_Space/AA_TEST/addressFrameCrasher.app/Contents/MacOS/addressFrameCrasher undefined reference to _NSVCardPboardType expected to be defined in Cocoa
Now what? We'll just print out NSVCardPboardType in gdb and notice it's the string @"Apple VCard pasteboard type". We'll provide our own copy so that it's defined even when it isn't by doing something a little nasty - which will break if they ever change the literal string! By doing this, we are able to provide a single version of the application that is able to run under 10.1 but still provide additional features under 10.2.
#define NSVCardPboardType_x @"Apple VCard pasteboard type"
and then use it like this to accept drag and drops of VCARDS in your view sub class:
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
NSString *type = [pboard availableTypeFromArray:[self acceptableDraggedTypes]];
if ([type isEqualToString:NSVCardPboardType_x]) {
NSData *data = [pboard dataForType:NSVCardPboardType_x];
if (data) [[MyDocument sharedInstanceOfClassName:@"AddressBookStubs"] loadPersonData:data intoDocument:[self document]];
} else if ([type isEqualToString:NSFilenamesPboardType]) { ...
Conclusion
Jaguar's new AddressBook features are cool - and if you're careful, you can make applications that run on either Mac OS X 10.1 or Jaguar and beyond!
|
|