The introductory section of this chapter informed you that you would use Contacts.framework to retrieve that app user's contacts and show this in a table view. To display a list of contacts, you must have access to the user's address book. Apple does a great job of protecting the user's privacy, so you can't read any of their contacts' data without asking the user for permission. Similar restrictions apply to access the user's camera, location, photos, and more.
Whenever you need access to privacy-sensitive information, you are required to specify this in your app's Info.plist file. This file keeps track of many of your app's properties, such as its display name, supported interface orientations, and, in the case of accessing a user's contacts, Info.plist also contains information about why you need access to the user's contacts.
To add this information to Info.plist, open it from the list of files in the Project Navigator on the left. Once opened, hover over the word Information Property List at the top of the file. A plus icon should appear, clicking it adds a new empty item with a search field to the list. When you begin typing Privacy – contacts, Xcode will filter out options for you until there is only one left for you to pick. This option, called Privacy – Contacts Usage Description, is the correct option to choose for this case. The value for this newly-added key should describe the reason that you need access to the specified piece of information for. In this case, reads contacts and shows them in a list should be sufficient explanation. When the user is asked for permission to access their contacts, the reason you specified here will be shown, so make sure you add an informative message.
Whenever you need access to photos, Bluetooth, the camera, and the microphone, make sure to check whether a privacy key in Info.plist is required. If you don't provide this key, your app will crash and will not make it past Apple's review process.
Now that you have configured your app so it specifies that it wants to access contact data, let's get down to writing some code. Before you can read contacts, you must make sure that the user has given the appropriate permissions for you to access contact data. To do this, the code must first read the current permission status. Once this is done, the user must either be prompted for permission to access contacts, or the contacts must be fetched. Add the following code to ViewController.swift, we'll cover the details for this code after you have implemented it:
import UIKit
// 1
import Contacts
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let store = CNContactStore()
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
// 2
if authorizationStatus == .notDetermined {
// 3
store.requestAccess(for: .contacts) { [weak self] didAuthorize,
error in
if didAuthorize {
self?.retrieveContacts(from: store)
}
}
} else if authorizationStatus == .authorized {
retrieveContacts(from: store)
}
}
func retrieveContacts(from store: CNContactStore) {
let containerId = store.defaultContainerIdentifier()
let predicate = CNContact.predicateForContactsInContainer(withIdentifier: containerId)
// 4
let keysToFetch = [CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactImageDataAvailableKey as
CNKeyDescriptor,
CNContactImageDataKey as CNKeyDescriptor]
let contacts = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
// 5
print(contacts)
}
}
In the preceding code, the first step is to import the Contacts framework into the current file. If you don't do this, the compiler won't be able to understand CNContactStore or CNContact because these classes are part of the Contacts framework.
The second step is to check the value of the current authorization status. For this example, only the notDetermined and authorized statuses are relevant. However, the user can also deny access to their address book. In that case, the authorization status would be denied. If the status has not been determined yet, the user is asked for permission. When the app already has access, the contacts are fetched right away.
In the third step, permission is asked to access the user's contacts. The request access method takes a completion-handler as its last argument. In asynchronous programming, completion-handlers are used often. It allows your app to perform some work in the background and then call the completion-handler when the work is completed. You will find completion-handlers throughout Foundation, UIKit, and many other frameworks. If you implement a very simple function of your own that takes a callback, it might look as follows:
func doSomething(completionHandler: (Int) -> Void) {
// perform some actions
var result = theResultOfSomeAction
completionHandler(result)
}
Calling a completion-handler looks just like calling a function. The reason for this is that a completion-handler is a block of code, called a closure. Closures are a lot like functions because they both contain a potentially reusable block of code that is expected to be executed when called. You will find plenty of examples of closures and completion-handlers in this book because they are ubiquitous in iOS, and programming in general.
Step four in the big chunk of code you added created a list of keys that you'll need to render a list of contacts. Since these keys are of the String type and you must provide a list of CNKeyDesriptor later, you can use as CNKeyDescriptor to convert these String values to CNKeyDescriptor values. Note that this won't always work because not every type is convertible to a specific other type. For example, you wouldn't be able to do this type of conversion with UIViewController.
Finally, when the contacts are fetched, they are printed to the console. Of course, you'll want to update this so that the contacts aren't printed in the console, but rendered in the table view. You might notice the try! keyword before fetching the contacts. This is done because fetching contacts could fail and throw an error.
In Swift, you are expected to make the right decision in regards to handling errors. By using try!, you inform the compiler that you are 100%, sure that this fetch call will never fail. This is fine for now so you can focus on the essential bits, but when you're writing an app that is expected to make it to production, you might want to handle errors more gracefully. A good practice is to use a do {} catch {} block for code that could throw an error. The following code shows a basic example of such a construct:
do {
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
print(contacts)
} catch {
// something went wrong
print(error) // there always is a "free" error variable inside of a catch block
}
If you run the app with the code you added, the app will immediately ask for permission to access contacts. If you allow access, you will see a list of contacts printed to the console, as shown in the following screenshot:
Now that you have the user's contact list, let's see how you can make the contacts appear in your table view!