The final frontier for the HelloContacts app is to display some actual information about a selected contact. In order to do this, we'll need to add some new outlets to the ContactDetailViewController
. The data that's loaded for contacts also needs to be expanded a little bit so a contact's phone number, e-mail address, and postal address are fetched. Finally, the contact data needs to be passed from the overview to the detail page so the detail page is able to actually display the data. The steps we'll take are as follows:
Currently, the code in ViewController.swift
specifies that just the given name, family name, image data, and image availability should be fetched. We need to expand this so the e-mail address, postal address, and phone number are fetched as well. Update the retrieveContacts(store:)
method with the following code:
let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactImageDataKey, CNContactImageDataAvailableKey, CNContactEmailAddressesKey, CNContactPhoneNumbersKey, CNContactPostalAddressesKey]
The new keys are highlighted so you can easily see what has changed.
Previously, you added some computed variables to the HCContact
class so you can easily access certain properties of the CNContact
class. We'll do the same for the news keys. Add the following code in HCContact.swift
:
var emailAddress: String { return String(contact.emailAddresses.first?.value ?? "--") } var phoneNumber: String { return String(contact.phoneNumbers.first?.value.stringValue ?? "--") } var address: String { let street = String(contact.postalAddresses.first?.value.street ?? "--") let city = String(contact.postalAddresses.first?.value.city ?? "--") return "(street), (city)" }
If you examine this code closely, you'll notice that the phoneNumber
and address
variables aren't as straight forward as the others. That's because the corresponding properties on CNContact
are arrays of NSValue
objects. Since we're only interested in the first item available, we use the first
property that's defined on the array. When available, it returns the first element in the array. Then, the value
property is used to extract the actual string value. The nil coalescing operator (??
) is then used to return either the retrieved value or a placeholder string. If the retrieved value doesn't exist, the placeholder is used instead.
For the postal address, the code is more complex. A postal address has multiple fields so we extract street
and city
with the same technique that's used for the phone number. Then, a string is returned with both values separated with a comma.
Now that we have exposed the new properties to users of the HCContact
class, it's time to pass it on to the detail page.
The transition from the overview page to the detail page is implemented with a segue. The segue is triggered whenever a user has selected a contact, and it takes care of getting the detail page on screen. As we're using a segue, there is a dedicated point that we can use to pass data from the overview to the detail page. This point is a method called prepare(for:sender:)
. This method is called whenever a segue is about to happen and it allows you to access the destination
. Let's implement it right now to see how it enables you to pass the selected contact to the detail page:
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) { (where keyword is replaced by a ,) if let contactDetailVC = segue.destination as? ContactDetailViewController where segue.identifier == "contactDetailSegue", let selectedIndex = collectionView.indexPathsForSelectedItems()?.first { contactDetailVC.contact = contacts[selectedIndex.row] } }
This implementation first verifies that the destination
is of the correct type. Then, it also makes sure that the segue's identifier
matches the identifier we set up for this segue. Finally, the first (and only) selected index path is read from the collection view. This information is then used to assign the contact from the contacts array to a contact
property on the instance of ContactDetailViewController
we're transitioning to. This property does not exist yet, but we'll implement it momentarily. This method is everything required to pass data from the overview to the detail page.
Using the data that's received on the detail page is pretty straightforward. We need to add some @IBOutlets
and a contact property. Then, we will use the information available in the contact at an appropriate time to show it to the user. Finally, the created outlets should be hooked up through the Interface Builder.
Let's take a look at the following required code first:
@IBOutlet var contactPhoneLabel: UILabel! @IBOutlet var contactEmailLabel: UILabel! @IBOutlet var contactAddressLabel: UILabel! var contact: HCContact? override func viewDidLoad() { // current implementation... if let contact = self.contact { contact.prefetchImageIfNeeded() contactImage.image = contact.contactImage contactNameLabel.text = "(contact.givenName) (contact.familyName)" contactPhoneLabel.text = contact.phoneNumber contactEmailLabel.text = contact.emailAddress contactAddressLabel.text = contact.address } }
This code should be added to the ContactDetailViewController.swift
file. The first part declares the outlets and the contact property. The additions to viewDidLoad
first check that the contact
is actually set and then assigns the values to the user interface elements.
The final step is to go to the storyboard, select the detail view controller, and connect the outlets in the Connections Inspector to the user interface elements. After doing this, build and run your app to see the detail page in its full glory.