Ever since iOS 10 was introduced, UITableView gained a performance optimization that can make a huge difference to a lot of apps. This feature is specified in a protocol named UITableViewDataSourcePrefetching. This protocol allows a data source to prefetch data before it is required. If your app is performing an expensive operation, such as downloading data from the internet or, as this contacts app does, decoding image data to display, prefetching will make the performance of UITableView a lot better.
Let's go ahead and implement prefetching in the HelloContacts app because we're currently decoding image data in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath). Decoding image data isn't a very fast operation, and it slows down the scrolling for your users. Kicking off this decoding a bit sooner in the prefetching stage will improve the scrolling performance of your users.
To conform to the UITableViewDataSourcePrefetching, you need to implement one method and add an extension for the UITableViewDataSourcePrefetching protocol. Update the code in ViewController.swift, as shown in the following code snippet:
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths:
[IndexPath]) {
for indexPath in indexPaths {
// we will implement the actual prefetching in a bit
}
}
}
The method that's implemented in this snippet receives a UITableView and an array on IndexPaths that should be prefetched as its arguments. Before you implement the actual prefetching logic, you'll need to think about the strategy you're going to apply to prefetching.
It would be ideal to have to decode each contact image only once. This can be solved by creating a class that holds the fetched CNContact instance, as well as the decoded image. This class should be named HCContact and should be set up so that you have to change a bare minimum of code in the ViewController.swift file.
Let's start by creating a new file (File | New | File...), and select the Swift file template. Name the file HCContact. Inside this file you should add the following code:
import UIKit
import Contacts
class HCContact {
private let contact: CNContact
var contactImage: UIImage?
var givenName: String {
return contact.givenName
}
var familyName: String {
return contact.familyName
}
init(contact: CNContact) {
self.contact = contact
}
func fetchImageIfNeeded() {
if let imageData = contact.imageData, contactImage == nil {
contactImage = UIImage(data: imageData)
}
}
}
There are two parts of this code that are interesting in particular. The first part is as follows:
var givenName: String {
return contact.givenName
}
var familyName: String {
return contact.familyName
}
These lines use computed properties to provide a proxy to the CNContact instance that is stored in this class. By doing this, you ensure that you don't have to rewrite the existing code that accesses these contact properties. Also, it prevents you from writing something such as contact.contact.givenName. Setting your properties up like this is good practice because you have detailed control over the exposed properties and you could easily swap out the underlying contact storage if needed.
The second part of this snippet that is interesting is:
func prefetchImageIfNeeded() {
if let imageData = contact.imageData, contactImage == nil {
contactImage = UIImage(data: imageData)
}
}
This method performs the decoding of the image data. It makes sure that the stored contact has image data available and it checks whether the contact image isn't set yet. If this is the case, the image data is decoded and assigned to contactImage. The next time this method is called, nothing will happen because contactImage won't be nil since the prefetching already did its job.
Now, make a few changes to ViewController.swift and you're good to go. The code snippet contains only the code where changes need to be made:
class ViewController: UIViewController {
var contacts = [HCContact]()
func retrieveContacts(fromStore store: CNContactStore) {
// ...
contacts = try! store.unifiedContacts(matching: predicate, keysToFetch:
keysToFetch).map { contact in
return HCContact(contact: contact)
}
tableView.reloadData()
}
}
extension UIViewController: UITableViewDataSource
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
// ...
contact.fetchImageIfNeeded()
if let image = contact.contactImage {
cell.contactImage.image = image
}
return cell
}
}
extension UIViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths:
[IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
contact.fetchImageIfNeeded()
}
}
}
First, we change the type of our contacts array from CNContact to HCContact, our own contact class. When retrieving contacts, we use Swift's powerful map method to convert the retrieved CNContacts to HCContacts.
Calling .map on an array allows you to transform every element in that array into something else. In this case, from CNContact to HCContact. When configuring the cell, fetchImageIfNeeded is called in case the table view did not call the prefetch method for this index path.
At this point, it's not guaranteed that the data for this cell has been prefetched. However, since the prefetching you implemented is pretty clever, this method can safely be called to make sure that the image is available. After all, the method does nothing if the image has already been prefetched. Then, we safely unwrap contactImage and then we set it on the cell.
In the prefetching method, the code loops over the IndexPaths we should prefetch data for. Each IndexPath consists of a section and a row. These properties match up with the sections and rows in the table view. When prefetching, a contact is retrieved from the contacts array, and we call fetchImageIfNeeded on it. This will allow the contact to decode the image data it contains before it needs to be displayed. This is all you have to do in order to optimize your UITableView for prefetching. Now let’s take a look at some of the UITableView delegate methods.