Book Image

Mastering iOS 12 Programming - Third Edition

By : Donny Wals
Book Image

Mastering iOS 12 Programming - Third Edition

By: Donny Wals

Overview of this book

The iOS development environment has significantly matured, and with Apple users spending more money in the App Store, there are plenty of development opportunities for professional iOS developers. However, the journey to mastering iOS development and the new features of iOS 12 is not straightforward. This book will help you make that transition smoothly and easily. With the help of Swift 4.2, you’ll not only learn how to program for iOS 12, but also how to write efficient, readable, and maintainable Swift code that maintains industry best practices. Mastering iOS 12 Programming will help you build real-world applications and reflect the real-world development flow. You will also find a mix of thorough background information and practical examples, teaching you how to start implementing your newly gained knowledge. By the end of this book, you will have got to grips with building iOS applications that harness advanced techniques and make best use of the latest and greatest features available in iOS 12.
Table of Contents (35 chapters)
Title Page
Copyright and Credits
Dedication
Packt Upsell
Contributors
Preface
Index

Under-the-hood performance of UITableView


Earlier in this chapter, you learned about cell-reuse in table views. You assigned a reuse-identifier to a table-view cell so that the table view would know which cell it should use to display contacts in. Cell-reuse is a concept that is applied so a table view can reuse cells that it has already created. This means that the only cells that are in memory are either on the screen or barely off the screen. The alternative would be to keep all cells in memory, which could potentially mean that hundreds or thousands of cells are held in memory at any given time. For a visualization of what cell reuse looks like, have a look at the following diagram:

As you can see, there are just a few cells in the picture that are not on the visible screen. This roughly equals the number of cells that a table view might keep in memory. This means that regardless of the total amount of rows you want to show, the table view has a roughly constant pressure on your app's memory usage.

Earlier, you witnessed a bug that showed the wrong image next to a contact in the table view. This bug is related to cell-reuse because the wrong image is actually only visible for contacts that don't have their own image. This means that the image from the contact that was previously shown in that particular cell is now shown for a different contact.

Note

If you haven't seen this bug occur because you don't have that many contacts in your list, try adding more contacts in the contacts app. Alternatively, you can implement a workaround to pretend that you have a lot more contacts. To do this, update tableView(_:numberOfRowsInSection:) so it returns contacts.count * 10. Also, update tableView(_:cellForRowAtIndexPath:) so the contact is retrieved as let contact = contacts[indexPath.row % contacts.count].

A cell is first created when dequeueReusableCell(withIdentifier:) is called on the table view and it does not have an unused cell available. Once the cell is either reused or created, prepareForReuse() is called on the cell. This is a great spot to reset your cells to their default state by removing any images or setting labels back to their default values. Next, tableView(_:willDisplay:forRowAt:) is called on the table views's delegate. This happens right before the cell is shown. You can perform some last-minute configuration here, but the majority of work should already be done in tableView(_:cellForRowAtIndexPath:). When the cell scrolls offscreen, tableView(_:didEndDisplaying:forRowAt:) is called on the delegate. This signals that a previously-visible cell has just scrolled out of the view's bounds.

With all this cell life cycle information in mind, the best way to fix the image-reuse bug is by implementing prepareForReuse() on ContactTableViewCell. Add the following implementation to remove any images that have previously been set:

override func prepareForReuse() {
  super.prepareForReuse()

  contactImage.image = nil
}

Quite an easy fix for a pesky bug, don't you think? Let's have a look at another performance optimization that table views have, called prefetching.

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.