Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

36. URLSession
Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 there’s 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 asynchronouys 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 master branch.

Branching 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. 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 master branch. Different people use different branching strategies but this is the general principle.

So far you have been committing your changes to the “master” 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 master branch.

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

The Source Control branch list
The Source Control branch list

Note: In the above screenshot, there are multiple branches already — one branch for each chapter of the book. If you have not created any branches till now, you should only see the master branch at your end.

➤ Select master — 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 “master”…:

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

Putting 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, 
      completionHandler: { 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
}

Handling status codes

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

Success! <NSHTTPURLResponse: 0x7f8b19e38d10> { URL: https://itunes.apple.com/search?term=metallica&limit=200 } { 
status code: 200, headers {
    "Cache-Control" = "no-transform, max-age=41";
    Connection = "keep-alive";
    "Content-Encoding" = gzip;
    "Content-Length" = 34254;
    "Content-Type" = "text/javascript; charset=utf-8";
    Date = "Fri, 21 Aug 2015 09:53:20 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"
Failure! <NSHTTPURLResponse: 0x7ff76b42d4b0> { URL: https://itunes.apple.com/searchLOL?term=metallica&limit=200 } { 
status code: 404, headers {
    Connection = "keep-alive";
    "Content-Length" = 207;
    "Content-Type" = "text/html; charset=iso-8859-1";
    . . .
} }
<!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>

Parsing 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
}

Handling 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()
}

Cancelling 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()
Failure! cancelled
2018-07-28 16:17:18.314825+0200 StoreSearch[21563:11484838] Task <FD358C0B-EE6B-4B10-8336-970CF1C2192D>.<2> finished with error - code: -999
if let error = error as NSError?, error.code == -999 {
  return  // Search was cancelled
} else if let httpResponse = . . .

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

Searching in the Software category
Qauzxmewt eb fgi Kadhqicu yayevotp

Adding the segmented control

➤ Open the storyboard. Drag a new Navigation Bar into the view and put it below the Search Bar. You’re using the Navigation Bar purely for decorative purposes, as a container for the segmented control.

The Segmented Control sits in a Navigation Bar below the Search Bar
Nqe Ratzagdaq Yegqpic loqc aw u Cejujimaem Bod zesuw dbi Bauyjw Heq

The finished Segmented Control
Pqi mewopyim Woxsojpex Rasqbiw

Using the assistant editor

➤ Press 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
Egsall ig oyseag muvhim xoc csu nodfazjar yivmtud

@IBAction func segmentChanged(_ sender: UISegmentedControl) {
  print("Segment changed: \(sender.selectedSegmentIndex)")
}
The segmented control in action
Dle dezhucmob yuwqquq uz oqmiom

Using the segmented control

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

tableView.contentInset = UIEdgeInsets(top: 108, left: 0, . . .
    let segmentColor = UIColor(red: 10/255, green: 80/255, blue: 80/255, alpha: 1)
    let selectedTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
    let normalTextAttributes = [NSAttributedString.Key.foregroundColor: segmentColor]
    segmentedControl.selectedSegmentTintColor = segmentColor
    segmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal)
    segmentedControl.setTitleTextAttributes(selectedTextAttributes, for: .selected)
    segmentedControl.setTitleTextAttributes(selectedTextAttributes, for: .highlighted)
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
Jue ler gur cujar qco wuihwr ma xumy o-zaitt

Setting the launch screen

➤ Remove the LaunchScreen.storyboard file from the project.

Downloading 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. Many developers have a tendency to stuff everything into their view controllers, but if you can move some of the logic into other objects, that makes for a much cleaner program.

// MARK:- Public 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, 
        completionHandler: { [weak self] url, response, 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
  }
}

Using 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(named: "Placeholder")
if let smallURL = URL(string: result.imageSmall) {
  downloadTask = artworkImageView.loadImage(url: smallURL)
}
The app now downloads the album artwork
Lga acg yom cunjdaobz cxe oxbar ugwqepc

App transport security

While your image downloading experience worked brilliantly 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, alongwith a ton of error messages for failed download tasks:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
Overriding App Transport Security
Uxizdaniny Ovx Ynuflfodn Kogufudc

Cancelling 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. This was a 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 master branch.

Merging your changes back to the master branch
Wityotq juid fdebfaj rayd hi kpu hohqob nwugsn

The confirmation dialog before merging changes
Cda zokqatlixiot cuomus tuvihe xevkagz gwatpiq

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 master
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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now