Improving performance with prefetching

In addition to UITableViewDelegate and UITableViewDataSource, a third protocol exists that you can implement to improve your table view's performance. It's called UITableViewDataSourcePrefetching and you can use it to enhance your data source. If your data source performs some complex task, such as retrieving and decoding an image, it could slow down the performance of your table view if this task is performed at the moment the table view wants to retrieve a cell. Performing this operation a little bit sooner than that can positively impact your app in those cases.

Since Hello-Contacts currently decodes contact images for its cells, it makes sense to implement prefetching to make sure the scrolling performance remains smooth at all times. The current implementation performs the decoding in tableView(_:cellForRowAt:). To move this logic to UITableViewDataSourcePrefetching, there is one method that needs to be implemented, it's called tableView(_:prefetchRowsAt:). Add the following extension to ViewController.swift to create a nice starting point for implementing prefetching:

extension ViewController: UITableViewDataSourcePrefetching {
  func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    for indexPath in indexPaths {
      // Prefetching will be implemented here soon
    }
  }
}

Instead of receiving just a single IndexPath, tableView(_:prefetchRowsAt:) receives a list of index path for which you should perform a prefetch. Before implementing the prefetching, take a step back to come up with a good strategy to implement prefetching. For instance, it would be ideal if each image only has to be decoded once to prevent duplicate work from being done. Also, a mechanism is needed to decode images in cases where the image wasn't prefetched. And also in that case, only having to decode once would be great. This can be achieved by creating a class that wraps CNContact and has some helper methods to make prefetching and decoding nice and smooth.

First, create a new file (File | New | File...) and select the Swift file template. Name this file Contact.swift. Add the following code to this file:

import UIKit
import Contacts

class Contact {
  private let contact: CNContact
  var image: UIImage?

  // 1
  var givenName: String {
    return contact.givenName
  }

  var familyName: String {
    return contact.familyName
  }

  init(contact: CNContact) {
    self.contact = contact
  }

  //2
  func fetchImageIfNeeded(completion: @escaping ((UIImage?) -> Void) = {_ in }) {
    guard contact.imageDataAvailable == true, let imageData = contact.imageData else {
      completion?(nil)
      return
    }

    if let image = self.image {
      completion?(image)
      return
    }

    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
      self?.image = UIImage(data: imageData)
      DispatchQueue.main.async {
        completion?(self?.image)
      }
    }
  }
}

The first thing to note about this code is the use of so-called computed variables. These variables act as a proxy for properties from the private CNContact instance that Contact wraps. It's good practice to set up proxies such as these because they prevent exposing too many details to other objects. Imagine having to switch from CNContact to a different type of contact internally. That becomes a lot easier when as few places as possible know about CNContact.

The second segment of code you should pay extra attention to is the image-fetching part that ensures we fetch images as efficiently as possible. First, the code checks whether an image is present on the contact at all. If it is, a check is done to see whether a decoded image already exists. And if it does, the completion closure is called with the decoded image. If no image exists yet, it is decoded on the global dispatch queue. By executing code on the global dispatch queue, it is automatically executed off the main thread. This means that no matter how slow or lengthy the image decoding gets, the table view will never freeze up because of it, since the main thread is not doing the heavy lifting. Because this code is asynchronous, a completion closure is used to call back with the decoded images. Calling back is done on the main thread since that is where the image should be used eventually. Note that the completion closure has a default value in the signature for fetchImageIfNeeded(completion:). Sometimes, the result of prefetching isn't needed so no completion handler will be given. Again, if this dispatch stuff makes you dizzy, don't worry. Or skip ahead to Chapter 25, Offloading Tasks with Operations and GCD, if you can't wait to learn more.

There are only a couple more changes that must be made to ViewController to make it uses the new Contact class and you're good to go. The following code snippet shows all the updates you will need to incorporate:

class ViewController: UIViewController {
  var contacts = [Contact]()

  //...

  func retrieveContacts(from store: CNContactStore) {
    //...

    // 1
    contacts = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
      .map { Contact(contact: $0) }

    // ...
  }
}

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return contacts.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ContactTableViewCell") as! ContactTableViewCell
    let contact = contacts[indexPath.row]

    cell.nameLabel.text = "(contact.givenName) (contact.familyName)"

    // 2
    contact.fetchImageIfNeeded { image in
      cell.contactImage.image = image
    }

    return cell
  }
}

extension ViewController: UITableViewDelegate {
  // extension implementation
}

extension ViewController: UITableViewDataSourcePrefetching {
  func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    for indexPath in indexPaths {
      // 3
      let contact = contacts[indexPath.row]
      contact.fetchImageIfNeeded()
    }
  }
}

The first addition is to make use of map to transform the list of CNContact instances to Contact instances. The second update uses the fetchImageIfNeeded(completion:) method to obtain an image. This method can be used because it has been set up to return either the existing decoded image or a freshly-decoded one if the prefetching wasn't able to decode the image in time.

The last change is to prefetch images as needed. Because fetchImageIfNeeded(completion:) has a default implementation for its completion argument, it can be called without a completion closure. The result isn't immediately relevant, so not having to provide a closure is convenient in this case. The prefetching is fully implemented now; you might not immediately notice any improvements when you run the app, but rest assured that proper use of prefetching can greatly benefit your apps.

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

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