Home iOS & Swift Books UIKit Apprentice

36
URLSession Written by Matthijs Hollemans & Fahim Farook

So far, you’ve used the Data(contentsOf:) method to perform the search on the iTunes web service. That is great for simple apps, but I want to show you another way to do networking that is more powerful.

iOS itself comes with a number of different classes for doing networking, from low-level sockets stuff that is only interesting to really hardcore network programmers, to convenient classes such as URLSession.

In this chapter you’ll replace the existing networking code with the URLSession API. That is the API the pros use for building real apps, but don’t worry, it’s not more difficult than what you’ve done before — just more powerful.

You’ll cover the following items in this chapter:

  • Branch it: Creating Git branches for major code changes.
  • Put URLSession into action: Use the URLSession class for asynchronous networking instead of downloading the contents of a URL directly.
  • Cancel operations: Canceling a running network request when a second network request is initiated.
  • Search different categories: Allow the user to select a specific iTunes Store category to search in instead of returning items from all categories.
  • Download the artwork: Download the images for search result items and display them as part of the search result listing.
  • Merge the branch: Merge your changes from your working Git branch back to your main branch.

Branch it

Whenever you make a big change to the code — such as replacing all the networking stuff with URLSession — there is a possibility that you’ll mess things up. I certainly do often enough! That’s why it’s smart to create a Git branch first.

The Git repository contains a history of all the app’s code, but it can also contain this history along different paths.

You just finished the first version of the networking code and it works pretty well. Now you’re going to completely replace that with a — hopefully — better solution. In doing so, you may want to commit your progress at several points along the way.

What if it turns out that switching to URLSession wasn’t such a good idea after all? Then you’d have to restore the source code to a previous commit from before you started making those changes. In order to avoid this potential mess, you can make a branch instead.

Branches in action
Branches in action

Every time you’re about to add a new feature to your code or have a bug to fix, it’s a good idea to make a new branch and work on that. When you’re done and are satisfied that everything works as it should, merge your changes back into the main branch. Different people use different branching strategies but this is the general principle.

So far you have been committing your changes to the “main” branch. Now you’re going to make a new branch, let’s call it “urlsession”, and commit your changes to that. When you’re done with this new feature you will merge everything back into the main branch.

You can find the branches for your repository in the Source Control navigator:

The Source Control branch list
The Source Control branch list

➤ Select main — or whatever is your current branch — from the branch list, and right-click on the branch name to get a context-menu with possible actions. Select Branch from “main”…:

The branch context-menu
The branch context-menu

➤ You will get a dialog asking for the new branch name. Enter urlsession as the new name and click Create.

Creating a new branch
Creating a new branch

When Xcode is done, you’ll see that a new “urlsession” branch has been added and that it is now the current one.

This new branch contains the exact same source code and history as the main branch, or whichever branch you used as the parent for the new branch. But from here on out the two paths will diverge — any changes you make happen on the “urlsession” branch only.

Put URLSession into action

Good, now that you’re in a new branch, it’s safe to experiment with these new APIs.

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    . . .
    searchResults = []
    // Replace all code after this with new code below
    // 1
    let url = iTunesURL(searchText: searchBar.text!)
    // 2
    let session = URLSession.shared
    // 3
    let dataTask = session.dataTask(with: url) {data, response, error in
      // 4
      if let error = error {
        print("Failure! \(error.localizedDescription)")
      } else {
        print("Success! \(response!)")
      }
    }
    // 5
    dataTask.resume()
  }
}

A brief review of closures

You’ve seen closures a few times now. They are a really powerful feature of Swift and you can expect to be using them all the time when you’re working with Swift code. So, it’s good to have at least a basic understanding of how they work.

let dataTask = session.dataTask(with: url, completionHandler: { 
   data, response, error in
   . . . source code . . .
})
{ parameters in 
  your source code
}
{ 
  your source code
}
let dataTask = session.dataTask(with: url, completionHandler: {
  (data: Data?, response: URLResponse?, error: Error?) in
  . . .
})
let dataTask = session.dataTask(with: url, completionHandler: { 
   data, _, error in
  . . .
})
let dataTask = session.dataTask(with: url, completionHandler: { 
   print("My parameters are \($0), \($1), \($2)")
})
let dataTask = session.dataTask(with: url) {
  data, response, error in
  . . .
}
lazy var dateFormatter: DateFormatter = {
  let formatter = DateFormatter()
  formatter.dateStyle = .medium
  formatter.timeStyle = .short
  return formatter
}()
let dataTask = session.dataTask(with: url, completionHandler: myHandler)
. . .

func myHandler(data: Data?, response: URLResponse?, error: Error?) {
  . . .
}
let dataTask = session.dataTask(with: url) {
  [weak self] data, response, error in
  . . .
}
let dataTask = session.dataTask(with: url) { data, response, error in
  self.callSomeMethod()     // self is required
}

Handle status codes

After a successful request, the app prints the HTTP response from the server. The response object might look something like this:

<NSHTTPURLResponse: 0x600003c7e6e0> { URL: https://itunes.apple.com/search?term=Knack&limit=200 } { Status Code: 200, Headers {
    "Cache-Control" =     (
        "max-age=86400"
    );
    "Content-Disposition" =     (
        "attachment; filename=1.txt"
    );
    "Content-Encoding" =     (
        gzip
    );
    "Content-Length" =     (
        35427
    );
    "Content-Type" =     (
        "text/javascript; charset=utf-8"
    );
    Date =     (
        "Sat, 22 Aug 2020 12:15:42 GMT"
    );
    . . . 
} }
if let error = error {
  print("Failure! \(error.localizedDescription)")
} else if let httpResponse = response as? HTTPURLResponse, 
  httpResponse.statusCode == 200 {
  print("Success! \(data!)")
} else {
  print("Failure! \(response!)")
}
} else if let httpResponse = response as? HTTPURLResponse {
  if httpResponse.statusCode == 200 {
    print("Success! \(data!)")
  }
Success! 295831 bytes
"https://itunes.apple.com/searchLOL?term=%@&limit=200"
<NSHTTPURLResponse: 0x600002792b00> { URL: https://itunes.apple.com/searchLOL?term=Jango&limit=200 } { Status Code: 404, Headers {
    "Cache-Control" =     (
        "private, max-age=300"
    );
    . . .
} }
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /searchLOL was not found on this server.</p>
</body></html>

Parse the data

➤ First, put iTunesURL(searchText:) back to the way it was — use ⌘+Z to undo.

if let data = data {
  self.searchResults = self.parse(data: data)
  self.searchResults.sort(by: <)
  DispatchQueue.main.async {
    self.isLoading = false
    self.tableView.reloadData()
  }
  return
}

Handle errors

➤ At the very end of the completion handler closure, below the if statements, add the following:

DispatchQueue.main.async {
  self.hasSearched = false
  self.isLoading = false
  self.tableView.reloadData()
  self.showNetworkError()
}

Cancel operations

What happens when a search takes a long time and the user starts a second search while the first one is still going? The app doesn’t disable the search bar, so it’s possible for the user to do this. When dealing with networking — or any asynchronous process, really — you have to think these kinds of situations through.

var dataTask: URLSessionDataTask?
dataTask = session.dataTask(with: url, completionHandler: {
dataTask?.resume()
dataTask?.cancel()
if let error = error as NSError?, error.code == -999 {
  return  // Search was cancelled
} else if let httpResponse = . . .

Search different categories

The iTunes store has a vast collection of products and each search returns at most 200 items. It can be hard to find what you’re looking for by name alone. So, you’ll add a control to the screen that lets users pick the category they want to search in. It will look like this:

Searching in the Software category
Nauwttest ic fvo Socfxaca beluwacf

Add the segmented control

➤ Open the storyboard. Drag a new Toolbar into the view and put it below the Search Bar. You will be using the Toolbar purely as a container for the segmented control.

The Segmented Control sits in a Toolbar below the Search Bar
Rqi Qeffacnex Buljkek gigw os a Naovyek fibul wgu Xiezns Ban

The finished Segmented Control
Jva sehihgac Lowhabtud Zijwqex

Use the assistant editor

➤ Press Control+Option+⌘+Enter to open the Assistant editor and then Control-drag from the Segmented Control into the view controller source code to add the new outlet:

@IBOutlet weak var segmentedControl: UISegmentedControl!
Adding an action method for the segmented control
Ohdatd uz ecgaat fefgag zih xyu fartoxlos ronlhot

@IBAction func segmentChanged(_ sender: UISegmentedControl) {
  print("Segment changed: \(sender.selectedSegmentIndex)")
}
The segmented control in action
Kpa baksukfiw cinyjuw uk ohweek

Use the segmented control

Notice that the first row of the table view is partially obscured again. Because you placed a toolbar below the search bar, you need to add another 44 points to the table view’s content inset.

tableView.contentInset = UIEdgeInsets(top: 94, left: 0, . . .
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  performSearch()
}
@IBAction func segmentChanged(_ sender: UISegmentedControl) {
  performSearch()
}
func iTunesURL(searchText: String, category: Int) -> URL {
  let kind: String
  switch category {
    case 1: kind = "musicTrack"
    case 2: kind = "software"
    case 3: kind = "ebook"
    default: kind = ""
  }
  let encodedText = searchText.addingPercentEncoding(
      withAllowedCharacters: CharacterSet.urlQueryAllowed)!
  let urlString = "https://itunes.apple.com/search?" + 
    "term=\(encodedText)&limit=200&entity=\(kind)"
  
  let url = URL(string: urlString)
  return url!
}
let url = iTunesURL(
  searchText: searchBar.text!, 
  category: segmentedControl.selectedSegmentIndex)
You can now limit the search to just e-books
Mie den sil qakij rni seikvj to wakc i-xeich

Set the launch screen

➤ Remove the LaunchScreen.storyboard file from the project.

Download the artwork

The JSON search results contain a number of URLs to images and you put two of those — imageSmall and imageLarge — into the SearchResult object. Now you are going to download these images over the Internet and display them in the table view cells.

SearchResultCell refactoring

First, you will move the logic for configuring the contents of the table view cells into the SearchResultCell class. That’s a better place for it. Logic related to an object should live inside that object as much as possible, not somewhere else.

// MARK: - Helper Methods
func configure(for result: SearchResult) {
  nameLabel.text = result.name
  
  if result.artist.isEmpty {
    artistNameLabel.text = "Unknown"
  } else {
    artistNameLabel.text = String(format: "%@ (%@)", result.artist, result.type)
  }
}
func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  if isLoading {
    . . .
  } else if searchResults.count == 0 {
    . . .
  } else {
    . . .
    let searchResult = searchResults[indexPath.row]
    // Replace all code after this with new code below
    cell.configure(for: searchResult)
    return cell
  }
}

UIImageView extension for downloading images

OK, here comes the cool part. You will now add an extension for UIImageView that downloads the image and automatically displays it via the image view on the table view cell with just one line of code!

import UIKit

extension UIImageView {
  func loadImage(url: URL) -> URLSessionDownloadTask {
    let session = URLSession.shared
    // 1
    let downloadTask = session.downloadTask(with: url) { 
      [weak self] url, _, error in
      // 2
      if error == nil, let url = url, 
        let data = try? Data(contentsOf: url),   // 3
        let image = UIImage(data: data) {
        // 4 
        DispatchQueue.main.async {
          if let weakSelf = self {
            weakSelf.image = image
          }
        }
      }
    }
    // 5
    downloadTask.resume()
    return downloadTask
  }
}

Use the image downloader extension

➤ Switch to SearchResultCell.swift and add a new instance variable, downloadTask, to hold a reference to the image downloader:

var downloadTask: URLSessionDownloadTask?
artworkImageView.image = UIImage(systemName: "square")
if let smallURL = URL(string: result.imageSmall) {
  downloadTask = artworkImageView.loadImage(url: smallURL)
}
The app now downloads the album artwork
Qfe osq nag womsbuajb yvi avdon irjxalz

App transport security

While your image downloading experience worked great here, sometimes when dealing with image downloads, or accessing any web URL for that matter, you might see something like the following in the Xcode Console, along with a ton of error messages like the following for failed download tasks:

The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
Overriding App Transport Security
Ubipgupabf Ayh Zjijpjasw Bulazapj

Cancel previous image downloads

These images already look pretty sweet, but you’re not quite done yet. Remember that table view cells can be reused, so it’s theoretically possible that you’re scrolling through the table and some cell is about to be reused while its previous image is still downloading.

override func prepareForReuse() {
  super.prepareForReuse()
  downloadTask?.cancel()
  downloadTask = nil
}

Caching

Depending on what you searched for, you may have noticed that many of the images were the same. For example, you might get many identical album covers in the search results. URLSession is smart enough not to download identical images — or at least images with identical URLs — twice. That principle is called caching and it’s very important on mobile devices.

Merge the branch

This concludes the section on talking to the web service and downloading images. Later on, you’ll tweak the web service requests a bit more to include the user’s language and country, but for now, you’re done with this feature. I hope you got a good glimpse of what is possible with web services and how easy it is to build this functionality into your apps using URLSession.

Merge the branch using Xcode

Now that you’ve completed a feature, you can merge this temporary branch back into the main branch.

Merging your changes back to the main branch
Genwuyf fiez yleypuy cowz mu bfi leav kmahwp

The confirmation dialog before merging changes
Qte pezpinwamuud weuvah jaxomo fofracd bzekkoz

Merge the branch from the command line

The source control features in Xcode used to be a bit rough around the edges. So, it was possible that certain commands, especially merging changes, might not work correctly. If Xcode didn’t want to cooperate when you tried to merge changes, here is how you’d do it from the command line.

git stash
git checkout main
git merge urlsession

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.