Adding protocols for clarity

We've already seen how protocols can be used to improve code by removing complex inheritance hierarchy. You also know how powerful it is to check for protocol conformance instead of checking whether a certain object is of a certain type. Let's see how we can improve and future-proof the HelloContacts application by adding some protocols.

We will define two protocols for now: one that specifies the requirements for any object that claims to be able to add a special animation to a view, and one that defines what it means to be able to be displayed as a contact.

Defining the view effect animator protocol

The first protocol we will define will be called ViewEffectAnimatorType. This protocol should be applied to any object that implements the required behaviors to animate a view. This protocol does not necessarily give us a direct advantage, but there are a few considerations that make this a very useful protocol.

A protocol is not only used to check whether or not an object can do something, it can also formalize a certain API that you came up with. In this case, we've decided that our BounceAnimationHelper needed certain initializers. It also needs to hold on to an animator, and it has a startAnimation method. Adding a protocol to this helper makes sure that any other helpers that conform to the same protocol have the same interface. This helps you, the developer, to make sense of what you should minimally implement for your new animation helper.

Another advantage is that we can move the startAnimation method to a protocol extension. Its implementation is simple and straightforward; we probably won't need to customize it later so it's a great candidate to provide a default implementation for. Create a new Swift file named ViewEffectAnimatorType, and add it to a new folder called Protocols. Now add the following implementation for the protocol:

import UIKit 
 
typealiasViewEffectAnimatorComplete = (UIViewAnimatingPosition) -> Void 
 
protocol ViewEffectAnimatorType { 
 
  var animator: UIViewPropertyAnimator { get } 
 
  init(targetView: UIView, onComplete: ViewEffectAnimatorComplete) 
  init(targetView: UIView, onComplete: ViewEffectAnimatorComplete, duration:      
  TimeInterval) 
 
  funcstartAnimation() 
} 
 
extension ViewEffectAnimatorType { 
  funcstartAnimation() { 
    animator.startAnimation() 
    } 
} 

This protocol defines all of the requirements we discussed before. Note that a globally available typealias named ViewEffectAnimatorComplete has been defined. This enables us to use the same handler type across our app, which enhances code consistency. To use this protocol, update the initializers for BounceAnimationHelper to use the new typealias and remove the old one. Also, remove the startAnimation method and finally add ViewEffectAnimatorType to the BounceAnimationHelper definition as shown in the following:

structBounceAnimationHelper: ViewEffectAnimatorType 

By adding this conformance, we use the protocol extension's default implementation for startAnimation and we have a predictable, formalized interface for the BounceAnimationHelper and any future effects that we may wish to add to our app. Let's add a protocol to our contact object as well.

Defining a contact display 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 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 where you're more interested in an object's capabilities rather 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.prefetchImageIfNeeded() 
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 prefetchImageIfNeeded 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 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 contactImage: UIImage? { get set } 
 
    mutating funcprefetchImageIfNeeded() 
} 

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.destinationViewController 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.prefetchImageIfNeeded() 
        if let image = contact.contactImage { 
        cell.contactImage.image = image 
        } 
 
        contacts[indexPath.row] = contact 
 
        return cell 
    } 

The changes you will 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 will need to change the contact property declaration from let to var because the prefetchImageIfNeeded method call will mutate the contact property.

Finally, ContactFetchHelper needs to be modified. Update the following line:

let contacts: [ContactDisplayable] = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch).map { contact in 
    return HCContact(contact: contact) 
}  

The important change here is that the contacts array type is now [ContactDisplayable] instead of [HCContact]. That's it; you can now build and run your project without errors!

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

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