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.
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.
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.
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
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 Navigation Controller
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 D
one
. 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!