Getting Started with Address Book - iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

Chapter 5. Getting Started with Address Book

The iOS Address Book frameworks have existed largely unchanged from their introduction in iOS 2.0 (then called iPhone OS 2.0). These frameworks were largely ported unchanged from their OSX counterparts, which have existed since OSX 10.2, making Address Book one of the oldest frameworks available on iOS. This legacy will become evident as you begin working with Address Book technology. It is largely seated on Core Foundation framework, which might seem unfamiliar to developers who have come over to Objective-C and Cocoa development after the introduction of the iPhone SDKs.

Why Address Book Support Is Important

When developing iOS software, you are running in an environment alongside your user’s mobile life. Users carry their mobile devices everywhere, and with these devices a considerable amount of their personal lives is intertwined with each device, from their daily calendar to personal photo albums. Paramount to this mobile life is the user’s contact information. This data has been collected over long periods, often on several devices, by a user and contains information about the user’s family, business, and social life.

An app can use a contact database to determine whether the user already has friends signed up for a service by parsing through a list of their email addresses or phone numbers to automatically add them as friends. Your app can also use a contact list for autopopulating emails or phone numbers or allow users to share their contact info with friends over Bluetooth (see Chapter 10, “Bluetooth Networking with Game Kit”). The reasons an app might need access to the user’s contacts are virtually endless.


Note

It is important to access the contact database only if your app has a legitimate reason to do so; nothing will turn a user off from your app more quickly than a breach of privacy.


Limitations of Address Book Programming

Although the Address Book frameworks remain fairly open, there are some important limitations to consider. The most notable, especially for those coming from the Mac development world, is that there’s no “me” card. Essentially, there is no way to identify your user in the list of contacts. Although there are some hacks that attempt to do this, nothing developed so far has proven to be reliable or is sanctioned by Apple.

A newer and welcome limitation—especially by privacy-concerned users—is the addition of Core Location–type authorization to access the contact database. This means that a user will be prompted to allow an app to access his contacts before being able to do so. When writing Address Book software, make an effort to ensure that your software continues to function even if a user has declined to let the app access his contact information.

Starting with iOS 6, a new privacy section exists in the Settings.app. From here, users are able to toggle on and off permissions to access Contacts, Locations, Calendars, Reminders, Photos, and Bluetooth.

Introduction to the Sample App

The sample app for this chapter is a simple address book viewer and editor. When launched, it will retrieve and display a list of all the contacts on your device. There is a plus button in the navigation bar for adding a new contact via the built-in interface, as well as a toggle button to change between showing either phone numbers or street addresses in the list. Additionally, the app has the capability to add a new contact programmatically and an example of using the built-in people picker.

Since the sample app, shown in Figure 5.1, is merely a base navigation controller project and does not have any overhead that is unrelated to Address Book programming, it is prudent to dive right into the functional code in the next section.

Image

Figure 5.1 A first look at the sample app.

Getting Address Book Up and Running

The first thing you need to do before working with the Address Book frameworks is to link both frameworks in your project. You need to be concerned with two frameworks: AddressBookUI.framework and AddressBook.framework. The first of these frameworks handles the graphical user interface for picking, editing, or displaying contacts, and the second handles all the interaction layers to work with that data. You need to import two headers, as shown here:

#import <AddressBook/AddressBook.h>
#import <AddressBookUI/AddressBookUI.h>

In the sample app, the headers are imported in RootViewController.h since the class will need to conform to several delegates, which is discussed later. You will also want to create a classwide instance of ABAddressBookRef, which in the sample app is called addressBook. The sample app also has an NSArray that will be used to store an array of the contact entries. It is a fairly expensive operation to copy the address book into memory, so you will want to minimize the number of times you will need to run that operation.

ABAddressBookRef addressBook;
NSArray *addressBookEntryArray;

To populate this new NSArray, you need to call ABAddressBookCreate. This will create a new instance of the address book data based on the current global address book database. In the sample app, this is done as part of the viewDidLoad method.

if(addressBook == NULL)
{
NSLog(@"Error loading address book: %@", CFErrorCopyDescription(creationError));
}

ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error)
{
if(!granted)
{
NSLog(@"No permission!");
}
});

You will also want to catch the event if our address book has no contacts, which will be default behavior on the iOS Simulator. The sample app displays a UIAlert in this situation to let the user know that the app isn’t broken, but that it has no data available to it. You can query the size of anABAddressBookRef with the function ABAddressBookGetPersonCount.

if(ABAddressBookGetPersonCount(addressBook) == 0)
{
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@""
message:@"Address book is empty!"
delegate:nil
cancelButtonTitle:@"Dismiss"
otherButtonTitles: nil];

[alertView show];
[alertView release];
}

Now you have a reference to the address book but you will want to translate that into a more manageable dataset. The sample app copies these objects into an NSArray since it will be using the data to populate a table view.

You have access to three functions for copying the address book data that will return a CFArrayRef. ABAddressBookCopyArrayOfAllPeople will return an array of all people in the referenced address book (this method is shown in the next code snippet).ABAddressBookCopyArrayOfAllPeopleInSource will return all the address book items that are found in a particular source. Lastly, ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering will allow you to sort the list of address book entries while retrieving it.

The sample app does not need to worry about sorting right now, so it simply retrieves the contacts with a call to ABAddressBookCopyArrayOfAllPeople. Since a CFArrayRef is a toll-free bridge to NSArray, it can be typecast and left with an NSArray. Now that you have an array of all the address book entries, it is just a simple task to get them displayed in a table.

addressBookEntryArray = (NSArray *) ABAddressBookCopyArrayOfAllPeople(addressBook);


Note

Sources in this context amount to where the contact was retrieved from; possible values can be kABSourceTypeLocal, kABSourceTypeExchange, kABSourceTypeMobileMe, and kABSourceTypeCardDAV. To get a list of all the sources found within the referenced address book, use ABAddressBookCopyArrayOfAllSources(addressBook). From there, you can query the sources that are of interest to your app.


Reading Data from the Address Book

In the preceding section, it was demonstrated how to populate an NSArray with entries from the user’s address book—each of these objects is an ABRecordRef. In this section, pulling information back out of an ABRecordRef is covered.

The sample app will be displaying the user data through a UITableView. There will be two types of values contained within the ABRecordRef: The first type is a single value used for objects for which there can be only one, such as a first and last name, and the second type is a multivalue used when dealing with values that a user might have more than one of, such as a phone number or a street address.

The following code snippet pulls an ABRecordRef from the address book array that was created in the preceding section and then retrieves the contact’s first and last name and sets NSString values accordingly. A full listing of available properties is shown in Table 5.1.

ABRecordRef record = [addressBookEntryArrayobjectAtIndex:indexPath.row];
NSString *firstName = (NSString *)ABRecordCopyValue(record, kABPersonFirstNameProperty);

NSString *lastName = (NSString *)ABRecordCopyValue(record, kABPersonLastNameProperty);

//...

if(firstName)
CFRelease(firstName);
if(lastName)
CFRelease(lastName);

Image

Table 5.1 Complete Listing of All Available Single-Value Constants in an ABRecordRef


Note

NARC (New, Alloc, Retain, Copy) is how I was taught memory management in the early days of Mac OS X programming, and the same holds true today for manual memory management. New advancements to memory management such as ARC are forever changing the way we handle manual memory management. However, we will not be using ARC in the sample app for this chapter. When you perform operations on Address Book with “copy” in the name, you must release that memory using a CFRelease() call. ARC enabled versions of the source code are available online.


Reading Multivalues from the Address Book

Often, you will encounter Address Book objects that can store multiple values, such as phone numbers, email addresses, or street addresses. These are all accessed using ABMultiValueRefs. The process is similar to that for single values with one additional level of complexity.

The first thing you need to do when working with multivalues, such as phone numbers, is copy the value of the multivalue property. In the following code example, use kABPersonPhoneProperty from the record that was set in the previous section. This provides you with anABMultiValueRef called phoneNumbers.

A check is then needed to make sure that the contact has at least one phone number using the ABMultiValueGetCount function. Here, you can either loop through all the phone numbers or pull the first one you find (as in the example). Additionally, you will want to handle the “no phone number found” case. From here, you need to create a new string and store the value of the phone number into it. This is done using the ABMultiValueCopyValueAtIndex call, the first parameter of the ABMultiValueRef followed by the index number.

ABMultiValueRef phoneNumbers = ABRecordCopyValue(record, kABPersonPhoneProperty);

if (ABMultiValueGetCount(phoneNumbers) > 0)
{
CFStringRef phoneNumber = ABMultiValueCopyValueAtIndex(phoneNumbers, 0);

NSLog(@"Phone Number: %@", phoneNumber);

CFRelease(phoneNumber);
}

CFRelease(phoneNumbers);

Understanding Address Book Labels

In the preceding section, you retrieved a phone number from the contact database; however, you know it only by its index number. Although this is helpful to developers, it is next to useless for users. You will want to retrieve the label that was used in the contact database. In the next code snippet, the example from the preceding section will be expanded on.

The first step to attaining a label for a multivalue reference is to call ABMultiValueCopyLabelAtIndex. Call this function with the same parameters you used to get the value of the multivalue object. This function will return a nonlocalized string, such as "_$!<Mobile>!$_". Although this is much more helpful than a raw index number, it is still not ready for user presentation.

You will need to run the returned label through a localizer to get a human-readable string. Do so using the ABAddressBookCopyLocalizedLabel using the raw value CFStringRef that was just set. In the example this will now return Mobile.

ABMultiValueRef phoneNumbers = ABRecordCopyValue(record, kABPersonPhoneProperty);

if (ABMultiValueGetCount(phoneNumbers) > 0)
{
CFStringRef phoneNumber = ABMultiValueCopyValueAtIndex(phoneNumbers, 0);

CFStringRef phoneTypeRawString = ABMultiValueCopyLabelAtIndex(phoneNumbers, 0);

NSString *localizedPhoneTypeString = (NSString *)ABAddressBookCopyLocalizedLabel(phoneTypeRawString);

NSLog(@"Phone %@ [%@]", phoneNumber, localizedPhoneTypeString);

CFRelease(phoneNumber);
CFRelease(phoneTypeRawString);
CFRelease(localizedPhoneTypeString);
}

Look back at the example in Figure 5.1—you now have the skill set to fully implement this functionality.

Working with Addresses

In the preceding two sections, you saw how to access single-value information and then how to access multivalue data. In this section, you will work with a bit of both as you learn how to handle street addresses that you encounter in the contact database. If you launch the sample app and tap the toggle button in the navigation bar, you will see that the addresses are now shown instead of phone numbers in the table cells (see Figure 5.2).

Image

Figure 5.2 The sample app showing addresses pulled from the contact database.

You will begin working with addresses in the same manner as you did for the phone multivalues. First attain an ABMultiValueRef for the kABPersonAddressProperty. Then you will need to make sure that at least one valid address was found. When you query the multivalue object with an index value, instead of getting back a single CFStringRef, you are returned a dictionary containing the address components. After you have the dictionary, you can pull out specific information using the address constants shown in Table 5.2.

ABMultiValueRef streetAddresses = ABRecordCopyValue(record,
kABPersonAddressProperty);

if (ABMultiValueGetCount(streetAddresses) > 0)
{
NSDictionary *streetAddressDictionary = (NSDictionary *)ABMultiValueCopyValueAtIndex(streetAddresses, 0);

NSString *street = [streetAddressDictionary objectForKey: (NSString *)kABPersonAddressStreetKey];

NSString *city = [streetAddressDictionary objectForKey: NSString *)kABPersonAddressCityKey];

NSString *state = [streetAddressDictionary objectForKey: (NSString *)kABPersonAddressStateKey];

NSString *zip = [streetAddressDictionary objectForKey: (NSString *)kABPersonAddressZIPKey];

NSLog(@"Address: %@ %@, %@ %@", street, city, state, zip);

CFRelease(streetAddressDictionary);
}

Image

Table 5.2 Address Components

Address Book Graphical User Interface

As mentioned in the beginning of this chapter, user interfaces are provided as part of the Address Book framework. In this section, a look is taken at those interfaces and how they can save an incredible amount of implementation time. Whether it is editing an existing contact, creating a new contact, or allowing your user to pick a contact from a list, Apple has you covered.

People Picker

You will undoubtedly want your user to be able to simply select a contact from a list. For example, let’s say you are writing an app that allows you to send a vCard over Bluetooth to another user; you will need to let your user select which contact card she wants to send. This task is easily accomplished using ABPeoplePickerNavigationController. You can turn on this functionality in the sample app by uncommenting line 63 ([self showPicker: nil];) in the RootViewController.m class.

Your class will first need to implement ABPeoplePickerNavigationControllerDelegate. You can then create a new picker controller using the following code snippet:

ABPeoplePickerNavigationController *picker = [[ABPeoplePickerNavigationController alloc] init];

picker.peoplePickerDelegate = self;
[self presentViewController:picker animated:YES completion:nil];
[picker release];

This displays a people picker to the user (see Figure 5.3).

Image

Figure 5.3 The built-in people picker.

You will also need to implement three delegate methods to handle callbacks from the user interacting with the people picker. The first method you need handles the Cancel button being tapped by a user; if you do not dismiss the modal in this method, the user has no way to dismiss the view.

- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker
{
[self dismissViewControllerAnimated:YES completion:nil];
}

When picking people, there are two sets of data you might be concerned with. The first is the contact itself, and by extension all the contact information. The second is a specific property, such as a specific phone number or email address from a contact. You can handle both of these cases. The first step will look at selecting an entire person’s contact information.

- (BOOL)peoplePickerNavigationController: (ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson: (ABRecordRef)person
{
NSLog(@"You have selected: %@", person);

[self dismissViewControllerAnimated:YES completion:nil];

return NO;
}

In this code snippet, NO is returned for peoplePickerNavigationController:shouldContinueAfterSelectingPerson:. This informs the picker that you do not intend to drill deeper into the contact and you only want to select an ABRecordRef for a person. As with the previous example, you must dismiss the modal view controller when you are done with it. If you do want to dive deeper, you will need to return YES here and implement the following delegate method. Do not forget to remove thedismissModalViewControllerAnimated:completion call from the previous method if you intend to drill deeper.

- (BOOL)peoplePickerNavigationController: (ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier
{

NSLog(@"Person: %@\nProperty:%i\nIdentifier:%i", person,property, identifier);

[self dismissViewControllerAnimated:YES completion:nil];

return NO;
}

Customizing the People Picker

There might be times when you want to allow the picker to choose only from phone numbers or street addresses and ignore the other information. You can do so by modifying the previous method of creating the people picker to match the following example, which will show only phone numbers.

ABPeoplePickerNavigationController *picker = [[ABPeoplePickerNavigationController alloc] init];

picker.displayedProperties = [NSArray arrayWithObject:[NSNumber numberWithInt:kABPersonPhoneProperty]];

picker.peoplePickerDelegate = self;
[self presentViewController:picker animated:YES completion:nil];

You also can specify a specific address book for the picker using the addressBook property. If you do not set this, a new address book is created for you when the people picker is presented.

Editing and Viewing Existing Contacts Using ABPersonViewController

Most of the time, you will want to simply display or edit an existing contact using the built-in Address Book user interfaces. In the sample app, this is the default action when a table cell is selected. You first create a new instance of ABPersonViewController and set the delegate and the person to be displayed, which is an instance of ABRecordRef. This approach will display the contact, as shown in Figure 5.4.

ABPersonViewController *personViewController = [[ABPersonViewController alloc] init];

personViewController.personViewDelegate = self;

personViewController.displayedPerson = personToDisplay;

[self.navigationController pushViewController:personViewController animated:YES];

[personViewController release];

Image

Figure 5.4 The built-in contact viewer.

If you want to allow editing of the contact, you simply add another property to the code snippet.

personViewController.allowsEditing = YES;

If you want to allow actions in the contact, such as Send Text Message or FaceTime buttons, you can add an additional allowsActions property.

personViewController.allowsActions = YES;

In addition to the steps you have already implemented, you need to be aware of one required delegate method, personViewController:shouldPerformDefaultActionForPerson:property:identifier. This is called when the user taps on a row such as a street address or a phone number. If you would like the app to perform the default action, such as call or open Maps.app, return YES; if you would like to override these behaviors, return NO.

- (BOOL)personViewController:(ABPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifierForValue
{

return YES;
}

Creating New Contacts Using ABNewPersonViewController

When you want to create a new contact, the sample app has a plus button in the navigation bar that allows you to create a new contact using the built-in user interfaces, shown in Figure 5.5. The next code snippet is straightforward with one caveat: The ABNewPersonViewControllermust be wrapped inside of a UINavigationController to function properly.

ABNewPersonViewController *newPersonViewController = [[ABNewPersonViewController alloc] init];

UINavigationController *newPersonNavigationController = [[UINavigationController alloc] initWithRootViewController:newPersonViewController];

[newPersonViewController setNewPersonViewDelegate: self];

[self presentViewController:newPersonNavigationController animated:YES completion:nil];

[newPersonNavigationController release];
[newPersonViewController release];

Image

Figure 5.5 The built-in new person view controller.

There is also a single delegate method that is called when the user saves the contact. After you verify that you have a valid person object being returned, you need to call ABAddressBookAddRecord with the address book you want to add the person into, followed byABAddressBookSave. If you have an array populated with the address book entries like the sample app, you will need to repopulate that array to see the changes.

- (void)newPersonViewController:(ABNewPersonViewController *)newPersonViewController didCompleteWithNewPerson:(ABRecordRef)person
{
if(person)
{
CFErrorRef error = NULL;

ABAddressBookAddRecord(addressBook, person, &error);
ABAddressBookSave(addressBook, &error);
if (error != NULL)
{
NSLog(@"An error occurred");
}
}

[self dismissViewControllerAnimated:YES completion:nil];

}

Programmatically Creating Contacts

What if you want to programmatically create a new contact instead of using the built-in graphical interface? Think about a contact-sharing app again. You don’t want to have to put the user through an interface when you can have the contact information entered programmatically.

In the sample project, uncomment line 66 ([self programmaticallyCreatePerson];) of the RootViewController.m and run it; you will notice that a new person appears in the contact list. The first step you will need to take in creating a new person is to generate a new empty ABRecordRef. You do this with the ABPersonCreate() method. You will also want to create a new NULL pointed CFErrorRef.

ABRecordRef newPersonRecord = ABPersonCreate();

CFErrorRef error = NULL;

Setting single-value properties is very straightforward, achieved by calling ABRecordSetValue with the new ABRecordRef as the first parameter, followed by the property constant, then the value, followed by the address of the CFErrorRef.

ABRecordSetValue(newPersonRecord, kABPersonFirstNameProperty, @"Tyler", &error);

ABRecordSetValue(newPersonRecord, kABPersonLastNameProperty, @"Durden", &error);

ABRecordSetValue(newPersonRecord, kABPersonOrganizationProperty, @"Paperstreet Soap Company", &error);

ABRecordSetValue(newPersonRecord, kABPersonJobTitleProperty, @"Salesman", &error);

Setting the phone number multivalue is slightly more complex than with a single-value object. You first need to create a new ABMutableMultiValueRef using the ABMultiValueCreateMutable() method with the type of multivalue property you are creating; in this instance, the phone property.

In the sample app, three different phone numbers are created, each with a different label property. After you finish adding new phone number values, you will need to call ABRecordSetValue with the new person record, the multivalue constant you are setting, and the mutable multivalue reference you just populated. Don’t forget to release the memory when you are done.

ABMutableMultiValueRef multiPhoneRef = ABMultiValueCreateMutable(kABMultiStringPropertyType);

ABMultiValueAddValueAndLabel(multiPhoneRef, @"1-800-555-5555", kABPersonPhoneMainLabel, NULL);

ABMultiValueAddValueAndLabel(multiPhoneRef, @"1-203-426-1234", kABPersonPhoneMobileLabel, NULL);

ABMultiValueAddValueAndLabel(multiPhoneRef, @"1-555-555-0123", kABPersonPhoneIPhoneLabel, NULL);

ABRecordSetValue(newPersonRecord, kABPersonPhoneProperty, multiPhoneRef, nil);

CFRelease(multiPhoneRef);

Email addresses are handled in the same manner as phone numbers. An example of an email entry is shown in the sample app. Street addresses are handled slightly differently, however.

You will still create a new mutable multivalue reference, but in this step, you also create a new mutable NSDictionary. Set an object for each key of the address that you want to set (refer to Table 5.2 for a complete list of values). Next, you need to add a label for this street address. In the code sample that follows, kABWorkLabel is used. When done, save the data in the same fashion as the phone or email entry.

ABMutableMultiValueRef multiAddressRef = ABMultiValueCreateMutable(kABMultiDictionaryPropertyType);

NSMutableDictionary *addressDictionary = [[NSMutableDictionary alloc] init];

[addressDictionary setObject:@"152 Paper Street" forKey:(NSString *) kABPersonAddressStreetKey];

[addressDictionary setObject:@"Delaware" forKey:(NSString *)kABPersonAddressCityKey];

[addressDictionary setObject:@"MD" forKey:(NSString *)kABPersonAddressStateKey];

[addressDictionary setObject:@"19963" forKey:(NSString *)kABPersonAddressZIPKey];

ABMultiValueAddValueAndLabel(multiAddressRef, addressDictionary, kABWorkLabel, NULL);

ABRecordSetValue(newPersonRecord, kABPersonAddressProperty, multiAddressRef, &error);

CFRelease(multiAddressRef); [addressDictionary release];

After you set up the new contact with all the information you want to enter, you need to save it and check for any errors that occurred during the process. In the sample app, the array and the table are reloaded to display the new entry.

ABAddressBookAddRecord(addressBook, newPersonRecord, &error);
ABAddressBookSave(addressBook, &error);

if(error != NULL)
{
NSLog(@"An error occurred");
}

Summary

This chapter covered the Address Book frameworks and how to leverage them into your iOS apps. You learned about the limitations and privacy concerns of the Address Book, as well as the importance of implementing it into appropriate apps.

Exploring the included sample app, you gained insightful and practical knowledge on how to get the Address Book frameworks quickly up and running. Additionally, you learned how to work with both retrieving and inserting new data into an address book both using Apple’s provided graphical user interface and programmatically. You should now have a strong understanding of the Address Book frameworks and be comfortable adding them into your iOS apps.

Exercises

1. Sort the address book by first or last name using the ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering method. Experiment with different sorting options.

2. Create a new person view controller, which allows the user to enter fields of his or her own. When the user is done entering information programmatically, save the new contact into the address book.