Book Image

Mastering iOS 12 Programming - Third Edition

By : Donny Wals
Book Image

Mastering iOS 12 Programming - Third Edition

By: Donny Wals

Overview of 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.
Table of Contents (35 chapters)
Title Page
Copyright and Credits
Dedication
Packt Upsell
Contributors
Preface
Index

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-ContactsViewController 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!