Under the hood of UITableView performance

Earlier in this chapter you briefly read about the cell reuse in UITableView. You had to assign a reuse identifier to your UITableViewCell so the UITableView knows which cell you want to use. This is done so UITableView can reuse existing cells. This means that UITableView only needs to hold the visible cells and some off-screen cells in memory instead of holding one cell for every data point, even if they're off screen. Refer to the following image for a visualization of what this looks like:

Under the hood of UITableView performance

You can see that there are only a couple more cells in memory, or rendered, than there are visible cells. The UITableView does that so it can display huge amounts of data without rendering the content slower or losing its scroll performance. So no matter how many rows you have in your UITableView, it will not use more system resources than needed. This optimization was especially important in the initial days of iOS because the older iPhones were even more memory constrained than the current versions are. Even though it's not as much of a necessity anymore, it's still one of the reasons users love iOS so much. It has amazing scroll performance.

If you've looked at our table view implementation really closely, you might have spotted an issue that's related to reusing cells. The contacts app displays other people's images for people who don't have images. Because cells are being reused, any property that you don't reset or overwrite in tableView(_:cellForRowAt:) will keep its previous value. Also, since not all contacts have an image and you only set the image if a contact does have one, it makes sense that you've encountered this bug.

For example, John, who has no image could be rendered into Jeff's old cell. Also, since John doesn't have an image, we don't overwrite Jeff's image at all and Jeff's image remains visible. In order to solve this cell reuse bug, we should have a look at the lifecycle of a UITableViewCell.

If you haven't seen this bug occur because you don't have a lot of contacts in the table view, try adding more contacts in the contacts app. Alternatively, you could implement a sneaky little workaround to pretend that there are a lot more contacts to display. To do this, update the tableView(_:numberOfRowsInSection:) method so it returns contacts.count * 10. Also, update tableView(_:cellForRow:AtIndexPath:) so the contact is retrieved as follows: let contact = contacts[indexPath.row % contacts.count].

A cell is first created when we ask the UITableView to dequeue a cell from the reuse queue. This happens when you call dequeueReusableCell(withIdentifier:). The UITableView will either reuse an existing cell or create a new one. When the cell is dequeued, the prepareForReuse method is called on the cell. This is the point where a cell should be reset to its default state. Next, tableView(_:willDisplay:forRowAt:) is called on UITableViewDelegate right before the cell is displayed. Some last minute configuration can be done here, but the majority should be already done while dequeuing the cell. Finally, the cell goes off screen and tableView(_:didEndDisplaying:forRowAt:) is called on UITableViewDelegate. This signals that a cell was on screen and has been scrolled off-screen now.

The fix for the image reuse bug is to implement prepareForReuse on your UITableViewCell because you may want to reset its imageView before it gets reused. Add the following code to ContactTableViewCell.swift:

override func prepareForReuse() { 
    super.prepareForReuse() 
 
    contactImage.image = nil 
} 

This method is called every time the UITableView reuses this cell. Make sure you call the superclass implementation as well by calling super.prepareForReuse() first. Then, set the image for the contactImage to nil. This will remove any image that is currently set on it and leave behind a clean cell that is reset in a way that prevents wrong images from showing up.

Improving performance with prefetching in iOS 10

With iOS 10, the UITableView gained a performance optimization that can make a huge difference to a lot of apps. The protocol that was added is called 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 of 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 UITableViewDataSourcePrefetching to the list of protocols we conform to. Update the code in ViewController.swift, as shown in the following code snippet:

class ViewController: UIViewController, UITableViewDataSource, 
  UITableViewDelegate, UITableViewDataSourcePrefetching { 
 
    // current implementation of ViewController 
 
    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 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 verbose and rather ugly such as contact.contact.givenName. Computed properties such as these allow you to create better APIs that improve readability and flexibility.

The second part of this snippet that is interesting is as follows:

func prefetchImageIfNeeded() { 
if let imageData = contact.imageData where 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. Make sure that you add UITableViewDataSourcePrefetching to the ViewController class declaration. The changes you need to make are emphasized in bold as follows:

class ViewController: UIViewController, UITableViewDataSource, 
  UITableViewDelegate, UITableViewDataSourcePrefetching { 
 
    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() 
    } 
 
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) 
      -> UITableViewCell { 
        // ... 
 
        contact.fetchImageIfNeeded() 
        if let image = contact.contactImage { 
            cell.contactImage.image = image 
        } 
 
        return cell 
    } 
 
    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 our prefetching 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.

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

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