Mastering iOS 12 Programming - Third Edition

4.3 (3 reviews total)
By Donny Wals
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. UITableView Touch-up

About 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.

Publication date:
October 2018
Publisher
Packt
Pages
750
ISBN
9781789133202

 

Chapter 1. UITableView Touch-up

There's a good chance that you have built a simple app before, or maybe you have tried but didn't quite succeed. If this is the case, it is likely that you have used UITableView or UITableViewController. UITableView is a core component of many iOS applications. If an app shows a list of things, it was likely built using UITableView. BecauseUITableViewis such an essential component on iOS, I want to make sure that we cover it right away in this book. It doesn't matter whether you have looked atUITableViewbefore. This chapter will ensure that you are up to speed with the ins and outs of UITableView and understand how Apple has made sure that every table view scrolls with a smooth speed of 60 frames per second, which we all strive for when developing apps.

In addition to covering the basics of UITableView, such as how it uses delegation to obtain information about the contents it should display, you'll also learn the basics about accessing a user's data, in this case, their contacts. The application that you will end up building displays a list of the user's contacts in a table view.

Every UITableView uses cells to render each item it displays. In this chapter, you will create your own UITableViewCell that uses Auto Layout. Auto Layout is a layout technique that is used throughout this book due to its essential part of every iOS developer's toolkit. If you haven't used Auto Layout before, or if you haven't heard of it, that's OK. This chapter starts with the basics, and your knowledge will expand as you go through this book.

In summary, this chapter covers:

  • Configuring and displaying UITableView
  • Fetching a user's contacts through Contacts.framework
  • The delegate and data source of UITableView
  • Creating a custom UITableViewCell
  • UITableView performance characteristics

 

 

Setting up the user interface


Every time you start a new project in Xcode, you have the option to pick a template for your application. Every template contains a small amount of code to get you started. Sometimes, a basic layout will even be set up for you. Throughout this book, you should default to using the Single View Application template. Don't be fooled by its name; you can add as many views to your app as you would like, this template only provides you with only one simple view. Using this template allows you to build your application from scratch, giving you the freedom to set up all the components as you like.

In this chapter, you will create an app that is called Hello-Contacts. This app renders your user's contact list in a UITableView that you will set up. Let's create a project for this app right now. Select File | New | Project in the menu bar. Next, select Single View Application from the list of project templates. When prompted to give your project a name, call it Hello-Contacts. Make sure that Swift is selected as the programming language for your app and uncheck all the Core Data and testing-related checkboxes; we won't need those right now.

Your configuration should resemble the following screenshot:

Once your application is configured, open the file named Main.storyboard. The storyboard file is used to lay out all of your application's views and to connect them to the code you write. The editor you use to manipulate your storyboard is called Interface Builder. Storyboards are a great way to edit your files and see the results of your actions immediately.

If you have used UITableView in the past, you may have used UITableViewController. The UITableViewController class is a subclass of a regular UIViewController. The difference is that UITableViewController contains a lot of setup that you would otherwise have to perform on your own. To fully understand how UITableView is configured and set up, you will not use UITableViewController now.

Take a look at the top bar in the Interface Builder window. There is a button there that has a circle with a square in it. This button opens the Object Library. The following screenshot shows you the Object Library and the button you can click to access it:

With the Object Library opened, look for UITableView. If you begin typing the name of the component you're looking for in the Object Library, all potential matches should automatically be filtered. Once you find the table view, drag it to the app's view. After doing this, use the white squares at the corners of the table view to make sure that the table covers the entire view.

If you look at the bottom of the window, you can see the dynamic viewport inspector. If you don't see it, try clicking on the name of the current preview device. In the inspector, select a device that either has a larger or a smaller screen than the current device. When you have done this, you will notice that the table view doesn't cover the viewport as nicely anymore. On smaller screens, you'll see that the table view has become larger than the view and on larger screens, the table view isn't large enough to cover the viewport:

To make sure your layout scales properly to fit any screen size you select, you use Auto Layout. Auto Layout enables you to create layouts that automatically adapt to any screen size that exists. Your layout currently uses fixed coordinates and dimensions to lay out the table view. For instance, your table view is set up to be positioned at (0, 0) with a size of (375, 667). This size is perfect for devices such as the iPhone 6, 6S, 7, and 8. But it wouldn't fit nicely with the iPhone Xs or iPad Pro. This combination of a view's position and size is called the frame.

Auto Layout uses constraints to define a layout instead of a frame. For instance, to make the table view fit the entire screen, you would add constraints that pin every edge of the table view to the corresponding edge of its superview. Doing so would make the table view match its superview's size at all times. The simplest way to set up constraints for this is to let Xcode add them for you. Let's use the dynamic viewport inspector to switch back to the initial device you had selected so we can add constraints from there.

First, ensure that the table view covers the entire viewport again, then click on the Resolve Auto Layout Issues button in the bottom-right corner of the Interface Builder screen and select Reset to Suggested Constraints from the menu that pops up:

Selecting this option automatically adds the constraints that Xcode considers required for your view. The constraints that were added by Xcode pin all of the table view's edges to its superview edges, which is precisely what you wanted to happen. You can manually inspect these constraints in the Document Outline on the left-hand side of the Interface Builder window:

Each constraint that got added describes how the table view should behave relative to another view. In this case, the other view is the superview. If you change the preview device in the dynamic viewport inspector, you should see that the table view always covers the entire screen. Try picking a couple of different screen sizes to make sure this works.

Now that you have a table view added to your view and its layout is exactly what you need, it is time to provide the table with some contents to display. To do this, you're going to write some code that uses Apple's Contacts.framework to fetch the app user's contacts list from their address book.

 

Fetching a user's contacts


The introductory section of this chapter informed you that you would use Contacts.framework to retrieve that app user's contacts and show this in a table view. To display a list of contacts, you must have access to the user's address book. Apple does a great job of protecting the user's privacy, so you can't read any of their contacts' data without asking the user for permission. Similar restrictions apply to access the user's camera, location, photos, and more.

Whenever you need access to privacy-sensitive information, you are required to specify this in your app's Info.plist file. This file keeps track of many of your app's properties, such as its display name, supported interface orientations, and, in the case of accessing a user's contacts, Info.plist also contains information about why you need access to the user's contacts.

To add this information to Info.plist, open it from the list of files in the Project Navigator on the left. Once opened, hover over the word Information Property List at the top of the file. A plus icon should appear, clicking it adds a new empty item with a search field to the list. When you begin typing Privacy – contacts, Xcode will filter out options for you until there is only one left for you to pick. This option, called Privacy – Contacts Usage Description, is the correct option to choose for this case. The value for this newly-added key should describe the reason that you need access to the specified piece of information for. In this case, reads contacts and shows them in a list should be sufficient explanation. When the user is asked for permission to access their contacts, the reason you specified here will be shown, so make sure you add an informative message.

Note

Whenever you need access to photos, Bluetooth, the camera, and the microphone, make sure to check whether a privacy key in Info.plist is required. If you don't provide this key, your app will crash and will not make it past Apple's review process.

Now that you have configured your app so it specifies that it wants to access contact data, let's get down to writing some code. Before you can read contacts, you must make sure that the user has given the appropriate permissions for you to access contact data. To do this, the code must first read the current permission status. Once this is done, the user must either be prompted for permission to access contacts, or the contacts must be fetched. Add the following code to ViewController.swift, we'll cover the details for this code after you have implemented it:

import UIKit
// 1
import Contacts

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    let store = CNContactStore()
    let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)

    // 2
    if authorizationStatus == .notDetermined {
      // 3
      store.requestAccess(for: .contacts) { [weak self] didAuthorize, 
      error in
        if didAuthorize {
           self?.retrieveContacts(from: store)
        }
      }
    } else if authorizationStatus == .authorized {
        retrieveContacts(from: store)
    }
  }

  func retrieveContacts(from store: CNContactStore) {
    let containerId = store.defaultContainerIdentifier()
    let predicate = CNContact.predicateForContactsInContainer(withIdentifier: containerId)
    // 4
    let keysToFetch = [CNContactGivenNameKey as CNKeyDescriptor,
                       CNContactFamilyNameKey as CNKeyDescriptor,
                       CNContactImageDataAvailableKey as 
                       CNKeyDescriptor,
                       CNContactImageDataKey as CNKeyDescriptor]

    let contacts = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)

    // 5
    print(contacts)
  }
}

 

 

In the preceding code, the first step is to import the Contacts framework into the current file. If you don't do this, the compiler won't be able to understand CNContactStore or CNContact because these classes are part of the Contacts framework.

The second step is to check the value of the current authorization status. For this example, only the notDetermined and authorized statuses are relevant. However, the user can also deny access to their address book. In that case, the authorization status would be denied. If the status has not been determined yet, the user is asked for permission. When the app already has access, the contacts are fetched right away.

In the third step, permission is asked to access the user's contacts. The request access method takes a completion-handler as its last argument. In asynchronous programming, completion-handlers are used often. It allows your app to perform some work in the background and then call the completion-handler when the work is completed. You will find completion-handlers throughout Foundation, UIKit, and many other frameworks. If you implement a very simple function of your own that takes a callback, it might look as follows:

func doSomething(completionHandler: (Int) -> Void) {
  // perform some actions
  var result = theResultOfSomeAction
  completionHandler(result)
}

Calling a completion-handler looks just like calling a function. The reason for this is that a completion-handler is a block of code, called a closure. Closures are a lot like functions because they both contain a potentially reusable block of code that is expected to be executed when called. You will find plenty of examples of closures and completion-handlers in this book because they are ubiquitous in iOS, and programming in general.

Step four in the big chunk of code you added created a list of keys that you'll need to render a list of contacts. Since these keys are of the String type and you must provide a list of CNKeyDesriptor later, you can use as CNKeyDescriptor to convert these String values to CNKeyDescriptor values. Note that this won't always work because not every type is convertible to a specific other type. For example, you wouldn't be able to do this type of conversion with UIViewController.

 

Finally, when the contacts are fetched, they are printed to the console. Of course, you'll want to update this so that the contacts aren't printed in the console, but rendered in the table view. You might notice the try! keyword before fetching the contacts. This is done because fetching contacts could fail and throw an error.

In Swift, you are expected to make the right decision in regards to handling errors. By using try!, you inform the compiler that you are 100%, sure that this fetch call will never fail. This is fine for now so you can focus on the essential bits, but when you're writing an app that is expected to make it to production, you might want to handle errors more gracefully. A good practice is to use a do {} catch {} block for code that could throw an error. The following code shows a basic example of such a construct:

do {
  let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
  print(contacts)
} catch {
  // something went wrong
  print(error) // there always is a "free" error variable inside of a catch block
}

If you run the app with the code you added, the app will immediately ask for permission to access contacts. If you allow access, you will see a list of contacts printed to the console, as shown in the following screenshot:

Now that you have the user's contact list, let's see how you can make the contacts appear in your table view!

 

Creating a custom UITableViewCell to show contacts

To display contacts in your table view, you must set up a few more things. First, you are going to need a table-view cell that displays contact information. All code for a custom table view cell should live in a UITableViewCell subclass. The design for your custom cell can be made in Interface Builder. When you make a design in Interface Builder, you can connect your code and the design using @IBOutlet. @IBOutlet is a connection between an object in the visual layout and a variable in your code.

Designing a table-view cell

Open your app's storyboard in Interface Builder and look for UITableViewCell in the Object Library. When you drag it into the table view that you have already added, your new cell is added as a prototype cell. A prototype cell functions as a blueprint for all cells the table view is going to display. That's right; you only need to set up a single cell to display many. You'll see how this works when you implement the code for your table-view cell. First, let's focus on the layout.

After dragging UITableViewCell to the table view, find and drag out UILabel and UIImageView. Both views should be added to the prototype cell. Arrange the label and image as shown in the following screenshot. After doing this, use the reset to suggested constraints feature you have used before to add Auto Layout constraints to the label and image. When you select both views after adding the constraints, you should see the same blue lines that are present in the following screenshot:

The blue lines from the image are a visual representation of the constraints that were added to lay out your label and image. In the image, you can see a constraint that offsets the label from the left side of the cell. Between the label and the image, you can see a constraint that defines the spacing between these two views. The line that runs through the cell horizontally shows that the label and image are centered on the vertical axis.

 

You can use Document Outline on the left side of Interface Builder to explore these constraints. The table-view cell design is now complete, it's time to implement the UITableViewCell subclass and create some @IBOutlets to connect design and code.

Creating the table-view cell subclass

To create a new UITableViewCell subclass, you need to create a new file (File | New | File) and choose a Cocoa Touch file. Name the file ContactTableViewCell and select UITableViewCell as the superclass for your file, as shown in the following screenshot:

When you open the newly-created file, you'll see that two methods were added to the ContactTableViewCell for you. These methods are awakeFromNib() and setSelected(_:animated:). The awakeFromNib() method is called the very first time an instance of your class is created. This method is the perfect place to do some initial setup that should only be performed once for your cell.

The second method in the template is setSelected(_:animated:), you can use this method to perform some customizations for the cell when a user taps on it. You could, for instance, change the text or background color for a cell there. For now, delete both methods from the class and replace its contents with the following code:

@IBOutlet var nameLabel: UILabel!
@IBOutlet var contactImage: UIImageView!

The preceding code should be the entire body for the ContactTableViewCell class. The variables in the class are annotated with @IBOutlet; this means that those variables can be connected with your prototype cell in Interface Builder. To do this, open Main. storyboard, select your prototype cell and look for the Identity Inspector in the sidebar on the right. Set the class property for your cell to ContactTableViewCell, as shown in the following screenshot. Setting this makes sure that your layout and code are correctly connected:

Next, select the table view cell that you added to the table view. Open the Connections Inspector in the right sidebar. Under the Outlets header, you'll find a list of names. Among those names, you can find the nameLabel and contactImage you added to ContactTableViewCell. Drag from the circle next to the nameLabel towards the label inside of your cell. By doing this, you connect the @IBOutlet that was created in code to its counterpart in the layout. Perform the same steps outlined in the preceding paragraph for the image, as shown in the following screenshot:

The last step is to provide a reuse-identifier for your cell. The table view uses the reuse-identifier so it can reuse instances of table-view cells. Cell-reuse is an optimization feature that will be cover in depth later in this chapter.

To set the reuse-identifier, open the Attributes inspector after selecting your cell. In the Attributes inspector, you'll find, and an input field labeled Identifier. Set this field to the ContactTableViewCell value.

With your layout fully set up, we need to take a couple more steps to make the table view show a list of contacts by assigning it a data source and delegate.

Displaying the list of contacts

One easily-overlooked fact about the table view is that no matter how simple it might seem to use one in your app, it's one of the more complex components of UIKit. Some of the complexity is exposed when you add a table view to a regular view controller instead of using UITableViewController. For instance, you had to manually set up the layout, so your table view covered the viewport. Then, you had to manually set up a prototype cell to display data in.

The next step toward displaying contacts to your user is providing the table view with information about the contents it should show. To do this, you must implement the data source and delegate for the table view. These properties use advanced concepts that you may have seen before, you probably just weren't aware of them yet. Let's make sure you know exactly what is going on.

Protocols and delegation

Throughout the iOS SDK and the Foundation framework, a design pattern named delegation is used. Delegation allows an object to have another object perform work on its behalf. When implemented correctly, it's a great way to separate concerns and decouple code within your app. The following figure illustrates how UITableView uses delegation for its data source using UITableViewDataSource:

The table view uses the help of two objects to function correctly. One is delegate, and the other is dataSource. Any time you use a table view, you must configure these two objects yourself. When the time comes for the table view to render its contents, it asks dataSource for information about the data to display. delegate comes into play when a user interacts with the items in the table view.

If you look at the documentation for UITableView, you can find the delegate property. The type for delegate is UITableViewDelegate?. This tells you two things about delegate. First of all, UITableViewDelegate is a protocol. This means that any object can act as a delegate for a table view, as long as it implements the UITableViewDelegate protocol. Second, the question mark behind the type name tells you that the delegate is an Optional property. An Optional property either has a value of the specified type, or it is nil. The table view's delegate is Optional because you do not haveto set it to create a functioning table view.

A protocol, such as UITableViewDelegate, defines a set of properties and methods that must be implemented by any type that wants to conform to the protocol. Not all methods must be explicitly implemented by conforming objects. Sometimes, a protocol extension provides a reasonable default implementation. You can read more about this in Chapter 6, Writing Flexible Code With Protocols And Generics.

In addition to delegate, UITableView has a dataSource property. The data source's type is UITableViewDataSource?, and just like UITableViewDelegate, UITableViewDataSource is a protocol. However, UITableViewDelegate only has optional methods, meaning you don't need to implement any methods to conform to UITableViewDelegate. UITableViewDataSource does have required methods. The methods that need to be implemented are used to provide the table view with just enough information to be able to display the correct amount of cells with the right content in them.

If this is the first time you're learning about protocols and delegation, you might feel a little bit lost right now. That's OK; you'll get the hang of it soon. Throughout this book, your understanding of topics such as these will improve bit by bit. You will even learn about a concept called protocol-oriented programming! For now, it's important that you understand that a table view asks a different object for the data it needs to show and that it also uses a different object to handle certain user interactions.

We can break the flow of displaying contents in a table view down into a couple of steps:

  1. The table view needs to reload the data
  2. The table view checks whether a dataSource is set, and asks it for the number of sections it should render
  3. Once the number of sections is passed back to the table view, the dataSource is asked for the number of items for each section
  4. With knowledge about the number of sections and items that need to be shown, the table view asks its dataSource for the cells it should display
  5. After receiving all of the configured cells, the table view can finally render these cells to the screen

These steps should give you a little bit more insight into how a table view uses another object to figure out the contents it should render. This pattern is compelling because it makes the table view an extremely flexible component. Let's put some of this newfound knowledge to use!

Conforming to the UITableViewDataSource and UITableViewDelegate protocols

To set up the table view's delegate and data source, you need to create an @IBOutlet for the table view in ViewController.swift. Add the following line to your ViewController class, just before viewDidLoad():

@IBOutlet var tableView: UITableView!

Now, using the same technique as you used before when connecting outlets for your table view cell, select the table view in Main.storyboard and use the Connections Inspector to connect the outlet to the table view.

To make ViewController both the delegate and the data source for its table view, it will have to conform to both protocols. It is a best practice to create an extension whenever you make an object conform to a protocol. Ideally, you make one extension for each protocol you want to implement. Doing this helps to keep your code clean and maintainable.

Add the following two extensions to ViewController.swift:

extension ViewController: UITableViewDataSource {
  // extension implementation
}
extension ViewController: UITableViewDelegate {
  // extension implementation
}

After doing this, your code contains an error. That's because none of the required methods from UITableViewDataSource have been implemented yet. There are two methods you need to implement to conform to UITableViewDataSource. These methods are tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:).

Let's go ahead and fix the error Xcode is showing by adjusting the code a little bit. This is also a great time to refactor the contact-fetching code a little bit. You will want to access the fetched contacts in multiple places, so the list should be an instance variable on the view controller. Also, if you're adding code to create cells anyway, you might as well make them display the correct information.

Add the following updates to ViewController.swift:

class ViewController: UIViewController {

  var contacts = [CNContact]()

  // viewDidLoad
  // retrieveContacts
}

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

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

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

    // 4
    if contact.imageDataAvailable == true, let imageData = contact.imageData {
      cell.contactImage.image = UIImage(data: imageData)
    }

    return cell
  }
}

The preceding code completes the implementation of UITableViewDataSource. Let's go over the commented sections of code to clarify them a little bit:

  • Since this table view only has a single section, the number of contacts as returned for the number of items in every section. This is OK because we know that there will always be just a single section. When you build an app that shows a table view with multiple sections, you would have to implement the numberOfSections property to inform the table view about the number of sections it needs to render.
  • This method is responsible for creating and configuring one of the ContactTableViewCell cells you created earlier.
  • Earlier in this chapter, you learned that cells are reused, that's why you had to set a reuse-identifier in the storyboard. Here, the reuse-identifier is used to ask for a cell to display a fetched contact in. Reusing cells that have been scrolled off screen is a performance optimization that enables a table view to display vast amounts of items without choppy scrolling or consuming tons of memory. The dequeueReusableCell(withIdentifier:) method has UITableViewCell as its return type. Therefore, you need to cast the result of that method to be the cell you set up in Interface Builder earlier. In this case, that is ContactTableViewCell.
  • The last step safely extracts image data from the contact if it's available. If it is, the image data is used to set up an image for the cell.

This doesn't wrap up the refactoring of fetching contacts just yet. Contacts are being fetched, but the array of contacts you added to ViewController earlier is not set up correctly yet, the fetched contacts are not attached to this array. In its current state, the last couple of lines in retrieveContacts look as follows:

let contacts = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
print(contacts)

Change these lines to the following code:

contacts = try! store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
DispatchQueue.main.async { [weak self] in
  self?.tableView.reloadData()
}

With this update, the result of fetching contacts is assigned to the variable you created earlier. Also, the table view is instructed to reload its data. Note that this is wrapped in a DispatchQueue.main.async call. Doing this ensures that the UI is updated on the main thread. Since iOS 11, your app will crash if you don't perform UI work on the main thread. If you want to learn more about this, have a look at Chapter 25, Offloading Tasks with Operations and GCD, it covers threading in more depth.

There is one more step before you're done. The table view is not aware of its dataSource and delegate yet. You should update the viewDidLoad() method to assign the table view's dataSource and delegate properties. Add the following lines to the end of viewDidLoad():

tableView.delegate = self
tableView.dataSource = self

Now go ahead and run your app. If you're running your app on the simulator, or you don't have any images assigned to your contacts, you won't see any images. You can add images to contacts in the simulator by dragging images from your Mac onto the simulator and saving them in the photo library. From there, you can add pictures to contacts the same way you would do it on a real device. If you have a lot of contacts on your device, but don't have an image for everybody, you might encounter an issue when scrolling. Sometimes, you might see a picture of a different contact than the one you expect! This is a performance optimization that is biting you. Let's see what's going on and how to fix this bug.

 

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.

 

UITableViewDelegate and interactions


So far, the ViewController class has implemented the UITableViewDelegate protocol but none of the delegate methods have been implemented yet. Any time certain interactions occur on a table view, such as tapping a cell or swiping on a cell, the table view will attempt to call the corresponding delegate methods to inform the delegate about the action that occurred.

The UITableViewDelegate protocol does not specify any required methods, which is why it has been possible to conform to this protocol without actually doing work. Usually, you will want to implement at least one method from UITableViewDelegate because simply displaying a list without responding to any interactions is quite boring. Let's go ahead and explore some of the methods from UITableViewDelegate to create a better experience for Hello-Contacts. If you take a look at the documentation for UITableViewDelegate, you'll notice that it has a large collection of delegate methods that you can implement in your app.

Note

You can hold the Alt key when clicking on a class, struct, enum, or protocol name to navigate to the documentation for the type you clicked on.

The documentation for UITableViewDelegate lists methods for configuring cell height, content-indentation level, cell-selection, and more. There are also methods that you can implement to get notified when the table view is about to display a cell or is about to stop displaying one. You can handle cell-selection, cell-highlighting, reordering cells, and even deleting them. One of the more common methods to implement is tableView(_:didSelectRowAt:). After you have implemented this method, you'll also implement cell-reordering and -removal.

Responding to cell-selection

Cell-selection refers to a user tapping on a cell. In order to respond to cell-selection, the UITableViewDelegate method called tableView(_:didSelectRowAt:) should be implemented. In Hello-Contacts, ViewController already has an extension implemented to make it conform to UITableViewDelegate so all you need to do is implement tableView(_:didSelectRowAt:) in the extension.

 

 

Since a table view will call methods on its delegate whenever they are implemented, you don't need to tell the table view that you want to respond to cell-selection. This automatically works if the table view has a delegate, and if the delegate implements tableView(_:cellForRowAt:). The implementation you'll add to Hello-Contacts, for now, is a very simple one. When the user taps a cell, the app displays an alert. In Chapter 3, Creating a Detail Page, you will learn how to perform more meaningful actions such as displaying a detail page. Add the following code to the UITableViewDelegate extension in ViewController.swift:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let contact = contacts[indexPath.row]
  let alertController = UIAlertController(title: "Contact tapped",
                                          message: "You tapped \(contact.givenName)",
                                          preferredStyle: .alert)

  let dismissAction = UIAlertAction(title: "Ok", style: .default, handler: { action in
    tableView.deselectRow(at: indexPath, animated: true)
  })

  alertController.addAction(dismissAction)
  present(alertController, animated: true, completion: nil)
}

The tableView(_:cellForRowAt:) method receives two arguments, the first is the table view that called this delegate method. The second argument is the index path at which the selection occurred. The implementation you wrote for this method uses the index path to retrieve the contact that corresponds with the tapped cell so the contact name can be shown in an alert. You could also retrieve the contact's name from the tapped cell. However, this is not considered good practice because your cells and the underlying data should be as loosely-coupled as possible. When the user taps the Ok button in the alert, the table view is told to deselect the selected row. If you don't deselect the selected row, the last tapped cell will always remain highlighted. Note that the alert is displayed by calling present(_:animated:completion:) on the view controller. Any time you want to make a view controller display another view controller, such as an alert controller, you use this method.

Even though this setup is not extremely complex, there is some interesting stuff going on. The delegation pattern is a very powerful one when implemented correctly. Especially in the case of a table view, you can add a lot of functionality simply by implementing the delegate methods that correspond to the desired behavior. Because the table view's delegate could be any object that conforms to UITableViewDelegate, you could split up ViewController and UITableViewDelegate entirely. Doing so would enable you to reuse your delegate implementation across multiple table views. For now, I'll leave it as an exercise for you to do this. Attempting such a refactor will certainly help you to increase your understanding of delegation and its powers.

Note

Try to extract your delegate and/or data source for the table view to a separate class or struct. This will allow you to reuse your code, and you will gain a deeper understanding of what delegation is and how it works.

Implementing cell-deletion

Now that you know how to respond to cell-selection, let's have a look at a slightly more advanced topic – cell-deletion. Deleting data from a table view is a feature that many apps implement. If you have ever used the mail app on iOS, you might have noticed that several actions appear when a user swipes either right or left on a table-view cell. These swipe actions are a great feature to implement for Hello-Contacts so users can swipe over contacts and easily delete them. Of course, we won't be actually deleting contacts from a user's address book, but it would be possible to implement this if you truly wanted to.

In this example, you'll learn how to delete contacts from the array of contacts that is used to populate the table view. To implement support for cell-deletion, you need to implement another method from UITableViewDelegate. The method you need to implement is tableView(_:trailingSwipeActionsConfigurationForRowAt:). This delegate method is called when a user swipes over a table view cell and returns the actions should be shown when the cell moves sidewards. A good example of this is found in the mail app on iOS.

 

 

Add the following implementation of tableView(_:trailingSwipeActionsConfigurationForRowAt:) to the UITableViewDelegate extension for ViewController:

func tableView(_ tableView: UITableView,
               trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

  // 1
  let deleteHandler: UIContextualActionHandler = { [weak self] action, view, callback in
    self?.contacts.remove(at: indexPath.row)`
    callback(true)
  }

  // 2
  let deleteAction = UIContextualAction(style: .destructive,
                                        title: "Delete", handler: deleteHandler)

  // 3
  return UISwipeActionsConfiguration(actions: [deleteAction])
}

If you run the app now and swipe from right to left over a table view cell, a delete button will appear from underneath the cell. In the code snippet, the first step is to set up a delete-handler that takes care of the actual deletion of the contact. This handler is a closure that is passed to the UIContextualAction instance that is created in step two. You have seen closures passed directly to method calls already, for instance as completion-handlers. However, you can also store a closure in a variable. This allows you to reuse your closure in several places and can make your code more readable. The third and last step is to create an instance on UISwipeActionsConfiguration and pass it the actions that you want to display. Since you can pass an array of actions, it is possible to show multiple actions when the user swipes over a cell. In this case, only a single action is added – the delete action.

Currently, tapping the delete button doesn't do much. While the contact is removed from the underlying data source, the table view itself doesn't update. Table views don't automatically stay in sync with their data sources. Add the following deleteHandler implementation to make sure the table view updates when the user taps the delete button:

let deleteHandler: UIContextualActionHandler = { [weak self] action, view, callback in
  self?.contacts.remove(at: indexPath.row)
  self?.tableView.beginUpdates()
  self?.tableView.deleteRows(at: [indexPath], with: .fade)
  self?.tableView.endUpdates()
  callback(true)
}

This new version of deleteHandler ensures that the table view updates itself by removing the row that the user has decided to remove. Note that the contacts array is updated before updating the table view. When you update the table view like this, it will verify that it is in sync with the data source, which is the contacts array in this case. If the data source does not contain the expected amount of sections or rows, your app will crash. So whenever you update a table view, make sure to update the data source first. Also, note the calls to beginUpdates and endUpdates. These methods make sure that the table view doesn't reload in the middle of being manipulated. This is especially useful if you're performing a lot of complex updates, such as moving cells, inserting new ones, and removing old ones all at the same time.

With cell-deletion out of the way, let's have a look at reordering cells.

Allowing the user to reorder cells

In some applications, it makes sense for users to be able to reorder cells that are shown in a table view, such as in a to-do list application or a grocery list. Implementing cell-reordering takes a couple of steps. First, a table view needs to be put in editing mode. When a table view is in editing mode, the user can begin sorting cells visually. Typically, a button in the top right or left corner of the screen is used to enter and exit the editing mode. The easiest way to make space for a button is by wrapping your ViewController in a UINavigationController. Doing this makes a navigation bar appear at the top of the screen that has space for a title, back button, and also for custom buttons such as the Edit/Done button we need to make the table view enter and exit the editing mode.

Placing the table view in editing mode is actually really simple if you know how. Every UIViewController instance has a setEditing(_:animated:) method. If you override this method, you can use it as an entry point to call setEditing(_:animated:) on the table view so it enters edit mode. Once this is implemented, you need to implement tableView(_:moveRowAt:to:) from UITableViewDelegate to commit the reordered cells to your data source by updating the contacts array.

First, open Main.storyboard so you can wrap the view controller in a navigation controller. When you have selected the view controller in your storyboard, click Editor | Embed In | Navigation Controller in the menu bar at the top of the screen. This will automatically embed the view controller in a navigation controller and configure everything as needed. To add the Edit/Done button, open ViewController.swift and add the following code to viewDidLoad:

navigationItem.rightBarButtonItem = editButtonItem

This line adds a UIBarButtonItem that automatically toggles itself and calls setEditing(_:animated:) on the view controller. Since it's set as rightBarButtonItem, it will appear on the right side of the navigation bar. If you go ahead and build the app now, you'll see that you have a button that toggles between a label that says Edit and Done. To put the table view in edit mode, you must override the setEditing(_:animated:) method in ViewController.swift, as follows:

override func setEditing(_ editing: Bool, animated: Bool) {
  super.setEditing(editing, animated: animated)

  tableView.setEditing(editing, animated: animated)
}

What this method does should be self-explanatory. Go ahead and run the app now. If you tap the Edit button, every cell suddenly displays a red circle – while this is interesting, it's not quite what's needed. Cells don't show reorder controls when in edit mode by default. Open Main.storyboard again and select your prototype cell. In the Attributes inspector, you'll find a checkbox named Shows Re-order controls. You want to make sure this checkbox is checked so you can reorder cells.

The final step to implementing this feature is adding tableView(_:moveRowAt:to:) in the UITableViewDelegate extension in ViewController.swift. This method will make sure that the contacts array is updated in the same way the cells are, ensuring that the table view and data source remain nicely in sync. Add the following code to the UITableViewDelegate extension in ViewController.swift:

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  let contact = contacts.remove(at: sourceIndexPath.row)
  contacts.insert(contact, at: destinationIndexPath.row)
}

Even though it's only two lines, this code updates the data source by moving a contact from its old position in the array to its new position. You now have everything in place to correctly reorder cells in a table view. Go ahead and try it out!

 

Summary


The Hello-Contacts app is complete for now. The next few chapters will focus on improving it with a new layout, a detail page, and a couple more changes. You've covered a lot of ground on the way toward mastering iOS. You've used Auto Layout and the Contacts framework, you learned about delegation and custom table view cells, and you implemented several delegate methods to implement various features on your table view.

If you want to learn more about UITableView, I don't blame you! The table view is a very powerful and versatile component in the iOS developer's toolkit. Make sure to explore Apple's documentation because there is a lot more to learn and study. One of the most important patterns you learned about is delegation. You'll find implementations of the delegate pattern throughout this book and UIKit. Next up? Converting the UITableView to its even more powerful and interesting sibling, UICollectionView.

 

Questions


  1. What happens if you don't provide a reason for wanting to access a user's contacts?

a) Nothingb) An empty alert is shownc) The app crashes

  1. What is a reuse-identifier on a table-view cell used for? 

a) It helps you identify cells by their nameb) It is useful for accessibilityc) It is used by the table view to optimize performance

  1. Where does a table view obtain information about the cells it displays from?

a) CellProvider b) UITableViewDataSource c) UITableViewDelegate

  1. How does a table view make sure to keep its memory footprint as small as possible?

a) It loads all cells at once b) It reuses cells that were displayed before c) It limits the number of cells it can handle

  1. What is a placeholder cell called in Interface Builder?

a) Prototype cell b) Placeholder cell c) Designer cell

  1. What is a connection between an item Interface Builder and a variable in code called?

a) @IBConnection  b) @IBInlet  c) @IBOutlet

  1. Where is the best place to reset a table-view cell?

a) In awakeFromNib() b) In reset() c) In prepareForReuse()

 

Further reading


About the Author

  • Donny Wals

    Donny Wals is a passionate, curious, iOS developer from The Netherlands. With several years of experience in building apps and sharing knowledge under his belt, Donny is a respected member of the iOS development community. Donny enjoys delivering talks on smaller and larger scales to share his knowledge and experiences with his peers. In addition to sharing knowledge, Donny loves learning more about iOS, Apple's frameworks and development in general. This eagerness to learn has made him into a versatile iOS developer with knowledge of a significant number of Apple's frameworks and tools. During WWDC you will often find Donny binge-watching the talks that Apple engineers deliver to introduce new features and frameworks.

    Browse publications by this author

Latest Reviews

(3 reviews total)
This book is not yet read all.
Great book for advanced iOS developers
Muy buena contenido del libro y buenas sus promociones

Recommended For You

Book Title
Access this book and the full library for just $5/m.
Access now