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 almost completely different.
You can achieve this with a simple protocol that defines what it means to be displayed in a certain way. It's a perfect example of the 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, we 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()
if let image = contact.contactImage {
cell.contactImage.image = image
}
From this, we 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 pretty specific to a real person, it's wise to combine the two in a new property:
displayName: This provides us with a bit more flexibility in terms of what kinds of object 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 contactImage: UIImage? { get set }
mutating func fetchImageIfNeeded()
}
Now add the following computed property to HCContact and make sure that you add conformance to ContactDisplayable in its definition. While you're at it, replace the class keyword for HCContact with struct. That's much nicer, since we don't need any of the reference-type semantics that a class has. Also, don't forget to mark prefetchImageIfNeeded() as mutating in order to conform to the protocol. Changing from a class to a struct will give you some compiler errors. We'll take a look at fixing those soon:
var displayName: String {
return "\(givenName) \(familyName)"
}
Next, update the declaration for the contacts array in ViewController.swift to look as follows (this will enable us to add any object that can be displayed as a contact to the array):
var contacts = [ContactDisplayable]()
The final adjustment in ViewController we will need to make is in prepare(for:sender:). Because our contacts are now ContactDisplayable instead of HCContact, we can't assign them to the detail view controller right away. Update the implementation as follows to typecast the ContactDisplayable to HCContact so it can be set on the detail view controller:
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
if let contactDetailVC = segue.destination as? ContactDetailViewController,
let selectedIndex = collectionView.indexPathsForSelectedItems?.first,
let contact = contacts[selectedIndex.row] as? HCContact,
segue.identifier == "contactDetailSegue" {
contactDetailVC.contact = contact
}
}
We're almost done. Just a few more changes to make sure that our project compiles again. The issues we're seeing are all related to the change from a class to a struct and the addition of the ContactDisplayable protocol. In ViewController.swift, update the collectionView(_:cellForItemAt:)method to look as follows:
func collectionView(_ collectionView: UICollectionView, cellForItemAtindexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "contactCell", for: indexPath) as! ContactCollectionViewCell
var contact = contacts[indexPath.row]
cell.nameLabel.text = contact.displayName
contact.fetchImageIfNeeded()
if let image = contact.contactImage {
cell.contactImage.image = image
}
contacts[indexPath.row] = contact
return cell
}
The changes you need to make are highlighted. Because HCContact is now a value type, pulling it from the array will create a copy. This means we will need to put it back into the array ourselves. If we don't do this, the prefetching of the image won't persist, which would be a shame. Whenever you need to perform tricks like this to make your value type work properly, it should be a red flag to you. Once you start doing this, you'll often end up with strange contraptions to make value types work throughout your code, and this will lead to all kinds of inconsistencies and bugs. This is a great example of a case where you should seriously consider replacing your value type with a reference type for clarity and simplicity.
Next, open up ContactDetailViewController.swift. In this file, we need to change the contact property declaration from let to var because the prefetchImageIfNeeded method call will mutate the contact property.
Also make sure you update the following line in previewingContext(_:viewControllerForLocation:).
viewController.contact = contact as? HCContact
Finally, ContactFetchHelper needs to be modified. Update the following lines:
typealias ContactFetchCallback = ([ContactDisplayable]) -> Void
private func retrieve(withCallback callback: ContactFetchCallback) {
// current implementation
let contacts: [ContactDisplayable] = retrievedContacts.map { contact in
return HCContact(contact: contact)
}
callback(contacts)
}
The important change here is that the contact array type is now [ContactDisplayable] instead of [HCContact]. That's it you can now build and run your project without errors!