Chapter 18. Contacts

The user’s contacts, which the user sees through the Contacts app, constitute a database that your code can access programmatically through the Contacts framework. You’ll need to import Contacts.

A user interface for interacting with the contacts database is provided by the Contacts UI framework. You’ll need to import ContactsUI.

Note

The Contacts framework, introduced in iOS 9, replaces the Address Book framework and the Address Book UI framework. The Address Book framework was an archaic C API without memory management information, so it was almost impossible to use in Swift, and it wasn’t all that usable in Objective-C either. The Address Book framework is not discussed in this edition.

Access to the contacts database requires user authorization. You’ll use the CNContactStore class for this. To learn what the current authorization status is, call the class method authorizationStatus(for:) with a CNEntityType of .contacts. To ask the system to put up the authorization request alert if the status is .notDetermined, call the instance method requestAccess(for:completionHandler:). The Info.plist must contain some text that the system authorization request alert can use to explain why your app wants access. The relevant key is “Privacy — Contacts Usage Description” (NSContactsUsageDescription). See “Music Library Authorization” for detailed consideration of authorization strategy and testing.

Contact Classes

Here are the chief object types you’ll be concerned with when you work with the user’s contacts:

CNContactStore

The user’s database of contacts is accessed through an instance of the CNContactStore class. You do not need to keep a reference to an instance of this class. When you want to fetch a contact from the database, or when you want to save a created or modified contact into the database, instantiate CNContactStore, do your fetching or saving, and let the CNContactStore instance vanish.

Tip

CNContactStore instance methods for fetching and saving information can take time. Therefore, they should be called on a background thread; for example, you might call DispatchQueue.global(qos:.userInitiated).async. For details about what that means, see Chapter 24.

CNContact

An individual contact is an instance of the CNContact class. Its properties correspond to the fields displayed in the Contacts app. In addition, it has an identifier which is unique and persistent. A CNContact that comes from the CNContactStore is immutable (its properties are read-only) and has no connection with the database; it is safe to preserve it and to pass it around between objects and between threads. To create your own CNContact, start with its mutable subclass, CNMutableContact; to modify an existing CNContact, call mutableCopy to make it a CNMutableContact.

The properties of a CNContact are matched by constant key names designating those properties. For example, a CNContact has a familyName property, and there is also a CNContactFamilyNameKey. This should remind you of MPMediaItem (Chapter 16), and indeed the purpose is similar: the key names allow you, when you fetch a CNContact from the CNContactStore, to state which properties of the CNContact you want populated. By limiting the properties to be fetched, you fetch more efficiently and quickly.

CNContactFormatter, CNPostalAddressFormatter

A formatter is an engine for displaying aspects of a CNContact as a string. For example, a CNContactFormatter whose style is .fullName assembles the name-related properties of a CNContact into a name string. Moreover, a formatter will hand you the key names of the properties that it needs in order to form its string, so that you can easily include them in the list of contact properties you fetch from the store.

Fetching Contact Information

Now let’s put it all together and fetch some contacts. When we perform a fetch, there are two parameters to provide in order to limit the information to be returned to us:

A predicate

An NSPredicate. CNContact provides class methods that will generate some common predicates; you are most likely to use predicateForContacts(matchingName:) and predicateForContacts(withIdentifiers:).

Keys

An array of objects adopting the CNKeyDescriptor protocol; such an object will be either a string key name such as CNContactFamilyNameKey or a descriptor (actually an instance of a hidden class called CNAggregateKeyDescriptor) provided by a formatter such as CNContactFormatter.

I’ll start by finding myself as a contact in my contacts database. To do so, I’ll first fetch all contacts whose name is Matt. I’ll call the CNContactStore instance method unifiedContacts(matching:keysToFetch:). To determine which resulting Matt is me, I don’t need more than the first name and the last name of those contacts, so those are the keys I’ll ask for. I’ll cycle through the resulting array of contacts in an attempt to find one whose last name is Neuburg. There are some parts of the process that I’m not bothering to show: we are using a CNContactStore fetch method, so everything should be done on a background thread, and the fetch should be wrapped in a do...catch construct because it can throw:

let pred = CNContact.predicateForContacts(matchingName: "Matt")
var matts = try CNContactStore().unifiedContacts(matching: pred,
    keysToFetch: [
        CNContactFamilyNameKey as CNKeyDescriptor,
        CNContactGivenNameKey as CNKeyDescriptor
])
matts = matts.filter{$0.familyName == "Neuburg"}
guard let moi = matts.first else {
    print("couldn't find myself")
    return
}

Alternatively, since I intend to cycle through the fetched contacts, I could call enumerateContacts(with:), which hands me contacts one at a time. The parameter is a CNContactFetchRequest, a simple value class; in addition to keysToFetch and predicate, it has some convenient properties allowing me to retrieve CNMutableContacts instead of CNContacts, to dictate the sort order, and to suppress the unification of linked contacts. I don’t need those extra features here, however. Again, assume we’re in a background thread and inside a do...catch construct:

let pred = CNContact.predicateForContacts(matchingName:"Matt")
let req = CNContactFetchRequest(
    keysToFetch: [
        CNContactFamilyNameKey as CNKeyDescriptor,
        CNContactGivenNameKey as CNKeyDescriptor
])
req.predicate = pred
var matt : CNContact? = nil
try CNContactStore().enumerateContacts(with:req) { con, stop in
    if con.familyName == "Neuburg" {
        matt = con
        stop.pointee = true
    }
}
guard var moi = matt else {
    print("couldn't find myself")
    return
}

The contact that I fetched in the preceding examples is only partially populated. That means I can’t use it to obtain any further contact property information. To illustrate, let’s say that I now want to access my email addresses. If I were to carry on directly from the preceding code by reading the emailAddresses property of moi, I’d crash because that property isn’t populated:

let emails = moi.emailAddresses // crash

If I’m unsure what properties of a particular contact are populated, I can test for safety beforehand with the isKeyAvailable(_:) method:

if moi.isKeyAvailable(CNContactEmailAddressesKey) {
    let emails = moi.emailAddresses
}

But even though I’m not crashing any more, I still want those email addresses. One solution, obviously, would have been to plan ahead and include CNContactEmailAddressesKey in the list of properties to be fetched. Unfortunately, I failed to do that. Luckily, there’s another way; I can go back to the store and repopulate this contact, based on its identifier:

moi = try CNContactStore().unifiedContact(withIdentifier: moi.identifier,
    keysToFetch: [
        CNContactFamilyNameKey as CNKeyDescriptor,
        CNContactGivenNameKey as CNKeyDescriptor,
        CNContactEmailAddressesKey as CNKeyDescriptor
])
let emails = moi.emailAddresses

Now let’s talk about the structure of the thing I’ve just obtained — the value of the emailAddresses property. It’s an array of CNLabeledValue objects. A CNLabeledValue has a label and a value (and an identifier). This class handles the fact that some contact attributes can have more than one value, each intended for a specific purpose (which is described by the label). For example, I might have a home email address and a work email address. These addresses are not keyed by their labels — we cannot, for example, use a dictionary here — because I can have, say, two work email addresses. Rather, the label is simply another piece of information accompanying the value. You can make up your own labels, or you can use the built-in labels. Under the hood, the built-in labels are very strange-looking strings like "_$!<Work>!$_", but there are also some constants that you can use instead, such as CNLabelWork. Carrying on from the previous example, I’ll look for all my work email addresses:

let workemails = emails.filter{ $0.label == CNLabelWork }.map{ $0.value }

Postal addresses are similar, except that their value is a CNPostalAddress. (Recall that there’s a CNPostalAddressFormatter, to be used when presenting an address as a string.) Phone number values are CNPhoneNumber objects. And so on.

To illustrate the point about formatters and keys, let’s say that now I want to present the full name and work email of this contact to the user, as a string. I should not assume either that the full name is to be constructed as givenName followed by familyName nor that those are the only two pieces that constitute it. Rather, I should rely on the intelligence of a CNContactFormatter:

let full = CNContactFormatterStyle.fullName
let keys = CNContactFormatter.descriptorForRequiredKeys(for:full)
moi = try CNContactStore().unifiedContact(withIdentifier: moi.identifier,
    keysToFetch: [
        keys,
        CNContactEmailAddressesKey as CNKeyDescriptor
])
if let name = CNContactFormatter.string(from: moi, style: full) {
    print("(name): (workemails[0])") // Matt Neuburg: [email protected]
}

One more thing to watch out for regarding contact properties: dates, such as a birthday, are not Dates. Rather, they are DateComponents. This is because they do not require full date information; for example, I know when someone’s birthday is without knowing the year they were born.

The user’s contacts database can change while your app is running. To detect this, register for the .CNContactStoreDidChange notification. The arrival of this notification means that any contacts-related objects that you are retaining, such as CNContact instances, may be outdated.

Saving Contact Information

All saving of information into the user’s contacts database involves a CNContactSave object. This object batches the proposed changes that you give it by using instance methods such as add(_:toContainerWithIdentifier:), update(_:), and delete(_:). You then hand the CNContactSave object over to the CNContactStore with execute(_:).

In this example, I’ll create a contact for Snidely Whiplash with a Home email [email protected] and add him to the contacts database. Yet again, assume we’re in a background thread and inside a do...catch construct:

let snidely = CNMutableContact()
snidely.givenName = "Snidely"
snidely.familyName = "Whiplash"
let email = CNLabeledValue(label: CNLabelHome,
    value: "[email protected]" as NSString)
snidely.emailAddresses.append(email)
snidely.imageData = UIImagePNGRepresentation(UIImage(named:"snidely")!)
let save = CNSaveRequest()
save.add(snidely, toContainerWithIdentifier: nil)
try CNContactStore().execute(save)

Sure enough, if we then check the state of the database through the Contacts app, our Snidely contact exists (Figure 18-1).

pios 3101
Figure 18-1. A contact created programmatically

Contact Sorting, Groups, and Containers

Contacts are naturally sorted either by family name or by given name, and the user can choose between them (in the Settings app) in arranging the list of contacts to be displayed by the Contacts app and other apps that display the same list. The CNContact class provides a comparator, through the comparator(forNameSortOrder:) class method, suitable for handing to NSArray methods such as sortedArray(comparator:). To make sure your CNContact is populated with the properties needed for sorting, call the class method descriptorForAllComparatorKeys. Your sort order choices (CNContactSortOrder) are:

  • .givenName

  • .familyName

  • .userDefault

Contacts can belong to groups, and the Contacts application in macOS provides an interface for manipulating contact groups — though the Contacts app on an iOS device does not. (The Contacts app on an iOS device allows contacts to be filtered by group, but does not permit editing of groups — creation of groups, addition of contacts to groups, and so on. I am not clear on the reason for this curious omission.) A group in the Contacts framework is a CNGroup; its mutable subclass, CNMutableGroup, allows you to create a group and set its name. All manipulation of contacts and groups — creating, renaming, or deleting a group, adding a contact to a group or removing a contact from a group — is performed through CNSaveRequest instance methods.

Contacts come from sources. A contact or group might be on the device or might come from an Exchange server or a CardDAV server. The source really does, in a sense, own the group or contact; a contact can’t belong to two sources. A complicating factor, however, is that the same real person might be listed in two different sources as two different contacts; to deal with this, it is possible for multiple contacts to be linked, indicating that they are the same person. This is why the methods that fetch contacts from the database describe the resulting contacts as “unified” — the linkage between linked contacts from different sources has already been used to assemble the information before you receive them as a single contact. In the Contacts framework, a source is a CNContainer. When I called the CNSaveRequest instance method add(_:toContainerWithIdentifier:) earlier, I supplied a container identifier of nil, signifying the user’s default container.

Contacts Interface

The Contacts UI framework puts a user interface, similar to the Contacts app, in front of common tasks involving the listing, display, and editing of contacts in the database. This is a great help, because designing your own interface to do the same thing would be tedious and involved. The framework provides two UIViewController subclasses:

CNContactPickerViewController

Presents a navigation interface, effectively the same as the Contacts app but without an Edit button: it lists the contacts in the database and allows the user to pick one and view the details.

CNContactViewController

Presents an interface showing the properties of a specific contact. It comes in three variants:

Existing contact

Displays the details, possibly editable, of an existing contact fetched from the database.

New contact

Displays editable properties of a new contact, allowing the user to save the edited contact into the database.

Unknown contact

Displays a proposed contact with a partial set of properties, for editing and saving or merging into an existing contact in the database.

CNContactPickerViewController

A CNContactPickerViewController is a UINavigationController. With it, the user can see a list of all contacts in the database, and can filter that list by group.

Warning

You do not need user authorization to use this view controller.

To use CNContactPickerViewController, instantiate it, assign it a delegate (CNContactPickerDelegate), and present it as a presented view controller:

let picker = CNContactPickerViewController()
picker.delegate = self
self.present(picker, animated:true)

That code works — the picker appears, and there’s a Cancel button so the user can dismiss it. When the user taps a contact, that contact’s details are pushed onto the navigation controller. And when the user taps a piece of information among the details, some default action is performed: for a postal address, it is displayed in the Maps app; for an email address, it becomes the addressee of a new message in the Mail app; for a phone number, the number is dialed; and so on.

However, we have provided no way for any information to travel from the picker to our app. For that, we need to implement the delegate method contactPicker(_:didSelect:). This method comes in two forms:

The second parameter is a CNContact

When the user taps a contact name, the contact’s details are not pushed onto the navigation controller. Instead, the delegate method is called, the tapped contact is passed to us, and the picker is dismissed.

The second parameter is a CNContactProperty

When the user taps a contact name, the contact’s details are pushed onto the navigation controller. If the user now taps a piece of information among the details, the delegate method is called, the tapped property is passed to us, and the picker is dismissed.

A CNContactProperty is a value class, consisting of a key, a value, a label, a contact, and an identifier. It can thus contain the information for any property. Note that the contact is itself a property of the CNContactProperty, so we can get the entire contact and all its properties from here.

(If we implement both forms of this method, it is as if we had implemented only the first form. However, it’s possible to change that using the predicateForSelectionOfContact property, as I’m about to explain.)

You can perform additional configuration of what information appears in the picker and what happens when it is tapped, by setting properties of the picker before you present it. These properties are all NSPredicates:

predicateForEnablingContact

The predicate describes the contact. A contact will be enabled in the picker only if the predicate evaluates to true. A disabled contact cannot be tapped, so it can’t be selected and its details can’t be displayed.

predicateForSelectionOfContact

The predicate describes the contact. If the predicate evaluates to true, tapping the contact calls the first delegate method (the parameter is the contact). Otherwise, tapping the contact displays the contact details.

predicateForSelectionOfProperty

The predicate describes the property (in the detail view). If the predicate evaluates to true, tapping the property calls the second delegate method (the parameter is a CNContactProperty). Otherwise, tapping the property performs the default action.

You can also determine what properties appear in the detail view, by setting the displayedPropertyKeys property.

For example, let’s say we want the user to pass us an email address, and that’s the only reason we’re displaying the picker. Then a reasonable configuration would be:

picker.displayedPropertyKeys =
    [CNContactEmailAddressesKey]
picker.predicateForEnablingContact =
    NSPredicate(format: "emailAddresses.@count > 0")

And we would then implement only the second form of the delegate method (the parameter is a CNContactProperty). Our code, in combination with the delegate method implementation and the property defaults that we have not set, effectively says: “Only enable contacts that have email addresses. When the user taps an enabled contact, show the details. In the details view, show only email addresses. When the user taps an email address, report it to the delegate method and dismiss the picker.”

It is also possible to enable multiple selection. To do so, we implement a different pair of delegate methods:

  • contactPicker(_:didSelect:) (the second parameter is an array of CNContact)

  • contactPicker(_:didSelectContactProperties:) (the second parameter is an array of CNContactProperty)

This causes a Done button to appear in the interface, and our delegate method is called when the user taps it.

Warning

The interface for letting the user select multiple properties, if incorrectly configured, can be clumsy and confusing, and can even send your app into limbo. Experiment carefully before deciding to use it.

CNContactViewController

A CNContactViewController is a UIViewController. It comes, as I’ve already said, in three flavors, depending on how you instantiate it:

  • Existing contact: init(for:)

  • New contact: init(forNewContact:)

  • Unknown contact: init(forUnknownContact:)

The first and third flavors display a contact initially, with an option to show a secondary editing interface. The second flavor consists solely of the editing interface.

You can configure the initial display of the contact in the first and third flavors, by means of these properties:

allowsActions

Refers to extra buttons that can appear in the interface if it is true — things like Share Contact, Add to Favorites, and Share My Location. Exactly what buttons appear depends on what categories of information are displayed.

displayedPropertyKeys

Limits the properties shown for this contact.

message

A string displayed beneath the contact’s name.

There are two delegate methods (CNContactViewControllerDelegate):

contactViewController(_:shouldPerformDefaultActionFor:)

Used by the first and third flavors, in the initial display of the contact. This is like a live version of the picker predicateForSelectionOfProperty, except that the meaning is reversed: returning true means that the tapped property should proceed to trigger the Mail app or the Maps app or whatever is appropriate. This includes the message and mail buttons at the top of the interface. You are handed the CNContactProperty, so you know what was tapped and can take action yourself if you return false.

contactViewController(_:didCompleteWith:)

Used by all three flavors. Called when the user dismisses the editing interface. If the user taps Done in the editing interface, you receive the edited contact, which has already been saved into the database. (If the user cancels out of the editing interface, then if this delegate method is called, the received contact will be nil.)

Warning

You do not need user authorization to use this view controller, and you cannot prevent the user from saving the edited contact into the database.

Existing contact

To display an existing contact in a CNContactViewController, call init(for:) with a CNContact. However, this call will crash your app unless this contact has already been populated with all the information needed to display it in this view controller. Therefore, CNContactViewController supplies a class method descriptorForRequiredKeys, and you will want to call it to set the keys when you fetch your contact from the store. For example:

let pred = CNContact.predicateForContacts(matchingName: "Snidely")
let keys = CNContactViewController.descriptorForRequiredKeys()
let snides = try CNContactStore().unifiedContacts(matching: pred,
    keysToFetch: [keys])
guard let snide = snides.first else {
    print("no snidely")
    return
}

We now have a sufficiently populated contact, snide, and can use it in a subsequent call to CNContactViewController’s init(for:).

Having instantiated CNContactViewController, you set its delegate (CNContactViewControllerDelegate) and push the view controller onto an existing UINavigationController’s stack.

An Edit button appears at the top right, and the user can tap it to edit this contact in a presented view controller — unless you have set the view controller’s allowsEditing property to false, in which case the Edit button is suppressed.

Here’s a minimal working example; I’ll display the Snidely Whiplash contact that I obtained earlier. Note that, even if we were in a background thread earlier when we fetched snide from the database, we need to be on the main thread now:

let vc = CNContactViewController(for:snide)
vc.delegate = self
vc.message = "Nyah ah ahhh"
self.navigationController?.pushViewController(vc, animated: true)

New contact

To use a CNContactViewController to allow the user to create a new contact, instantiate it with init(forNewContact:). The parameter can be nil, or it can be a CNMutableContact that you’ve created and partially populated; but your properties will be only suggestions, because the user is going to be launched directly into the contact editing interface and can change anything you’ve put.

Having set the view controller’s delegate, you then do a little dance: you instantiate a UINavigationController with the CNContactViewController as its root view controller, and present the navigation controller. Thus, this is a minimal implementation:

let con = CNMutableContact()
con.givenName = "Dudley"
con.familyName = "Doright"
let npvc = CNContactViewController(forNewContact: con)
npvc.delegate = self
self.present(UINavigationController(rootViewController: npvc),
    animated:true)
Warning

You must dismiss the presented navigation controller yourself in your implementation of contactViewController(_:didCompleteWith:).

Unknown contact

To use a CNContactViewController to allow the user to edit an unknown contact, instantiate it with init(forUnknownContact:). You must provide a CNContact parameter, which you may have made up from scratch using a CNMutableContact. You must set the view controller’s contactStore to a CNContactStore instance; it’s not an error otherwise, but the view controller is useless if you don’t. You then set a delegate and push the view controller onto an existing navigation controller:

let con = CNMutableContact()
con.givenName = "Johnny"
con.familyName = "Appleseed"
con.phoneNumbers.append(CNLabeledValue(label: "woods",
    value: CNPhoneNumber(stringValue: "555-123-4567")))
let unkvc = CNContactViewController(forUnknownContact: con)
unkvc.message = "He knows his trees"
unkvc.contactStore = CNContactStore()
unkvc.delegate = self
unkvc.allowsActions = false
self.navigationController?.pushViewController(unkvc, animated: true)

The interface contains two buttons:

Create New Contact

The editing interface is presented, with a Cancel button and a Done button.

Add to Existing Contact

The picker is presented. The user can tap Cancel or tap an existing contact. If the user taps an existing contact, that contact is presented for editing, with fields from the partial contact merged in, along with a Cancel button and an Update button.

If the framework thinks that this partial contact is the same as an existing contact, there will be a third button offering explicitly to update that particular contact. The result is as if the user had tapped Add to Existing Contact and picked this existing contact: the editing interface for that contact appears, with the fields from the partial contact merged in, along with Cancel and Update buttons.

In the editing interface, if the user taps Cancel, you’ll never hear about it; contactViewController(_:didCompleteWith:) won’t even be called.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.113.147