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

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.