Defining a contact-displayable protocol

Many apps display lists of contents that are almost the same, but not quite. Imagine displaying a list of contacts: a placeholder for a contact that can be tapped to add a new contact and other cells that could suggest people you may know. Each of these three cells in the collection view could look the same, yet the underlying models can be very different.

A certain sense of unity among these three models can be achieved with a simple protocol that defines what it means to be displayed in a certain way. It's a perfect example of a situation where you're more interested in an object's capabilities than its concrete type. To determine what it means to be displayed in the contact overview, you should look inside ViewController.swift. The following code is used to configure a cell in the contact-overview page:

let contact = contacts[indexPath.row]

cell.nameLabel.text = "(contact.givenName) (contact.familyName)"
contact.fetchImageIfNeeded { image in cell.contactImage.image = image }

From this code, you can extract four things a contact-displayable item should contain:

  • A givenName property
  • A familyName property
  • A fetchImageIfNeeded method
  • A contactImage property

Since givenName and familyName are specific to a real person, it's wise to combine the two in a new property: displayName. This provides some more flexibility regarding what kinds of objects can conform to this protocol without having to resort to crazy tricks. Create a new Swift file named ContactDisplayable and add it to the Protocols folder. Add the following implementation:

import UIKit   

protocol ContactDisplayable {
  var displayName: String { get }
  var image: UIImage? { get set }

  func fetchImageIfNeeded()
  func fetchImageIfNeeded(completion: @escaping ((UIImage?) -> Void))
}

Now add the following computed property to Contact and make sure that you add conformance to ContactDisplayable in the Contact class's definition:

var displayName: String {   
   return "(givenName) (familyName)"   
} 

You may have noticed that the protocol contains two fetchImageIfNeeded() declarations, one with the completion closure and one without. Unfortunately, you can't provide default parameters for function arguments in protocols, so to keep code changes to a minimum, you must specify both versions of fetchImageIfNeeded(). Update Contact by adding fetchImageIfNeeded() without arguments, as follows:

func fetchImageIfNeeded() {
  fetchImageIfNeeded(completion: { _ in })
}

Also, update the signature for fetchImageIfNeeded(completion:) by removing its default completion closure:

func fetchImageIfNeeded(completion: @escaping ((UIImage?) -> Void)) {
  // existing implementation
}

Next, update the declaration for the contacts array in ViewController.swift to look as follows (this will allow you to add any object that can be displayed as a contact to the array):

var contacts = [ContactDisplayable]()

The next change you need to make in ViewController is in prepare(for:sender:). Because the contacts are now ContactDisplayable instead of Contact, you can't assign them to the detail view controller right away. Update the implementation as follows to typecast the ContactDisplayable item to Contact so it can be set on the detail view controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let contactDetailVC = segue.destination as? ContactDetailViewController,
    segue.identifier == "detailViewSegue",
    let selectedIndex = collectionView.indexPathsForSelectedItems?.first,
    let contact = contacts[selectedIndex.row] as? Contact {
      contactDetailVC.contact = contact
  }
}

You're almost done. Just a few more changes to make sure that the project compiles again. The issues you see right now are all related to the change from a class to a struct and to the addition of the ContactDisplayable protocol. In ViewController.swift, update the collectionView(_:cellForItemAt:) method to look as follows:

func collectionView(_ collectionView: UICollectionView,
                    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
    "ContactCollectionViewCell", for: indexPath) as! ContactCollectionViewCell

  let contact = contacts[indexPath.row]
  // 1
  cell.nameLabel.text = "(contact.displayName)"
  contact.fetchImageIfNeeded { image in
    cell.contactImage.image = image
  }

  return cell
}

Lastly, make sure to update the following line in previewingContext(_:viewControllerForLocation:):

viewController.contact = contact as? Contact
..................Content has been hidden....................

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