Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering iOS 12 Programming

You're reading from   Mastering iOS 12 Programming Build professional-grade iOS applications with Swift and Xcode 10

Arrow left icon
Product type Paperback
Published in Oct 2018
Publisher Packt
ISBN-13 9781789133202
Length 750 pages
Edition 3rd Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Donny Wals Donny Wals
Author Profile Icon Donny Wals
Donny Wals
Arrow right icon
View More author details
Toc

Table of Contents (29) Chapters Close

Preface 1. UITableView Touch-up 2. A Better Layout with UICollectionView FREE CHAPTER 3. Creating a Detail Page 4. Immersing Your Users with Animation 5. Understanding the Swift Type System 6. Writing Flexible Code with Protocols and Generics 7. Improving the Application Structure 8. Adding Core Data to Your App 9. Fetching and Displaying Data from the Network 10. Being Proactive with Background Fetch 11. Syncing Data with CloudKit 12. Using Augmented Reality 13. Improving Apps With Location Services 14. Making Smarter Apps with CoreML 15. Tracking Activity Using HealthKit 16. Streamlining Experiences with Siri 17. Using Media in Your App 18. Implementing Rich Notifications 19. Instant Information with a Today Extension 20. Exchanging Data With Drag And Drop 21. Improved Discoverability with Spotlight and Universal Links 22. Extending iMessage 23. Ensuring App Quality with Tests 24. Discovering Bottlenecks with Instruments 25. Offloading Tasks with Operations and GCD 26. Submitting Your App to the App Store 27. Answers 28. Other Books You May Enjoy

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.

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.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime