URLSession Tutorial: Getting Started

In this URLSession tutorial, you’ll learn how to create HTTP requests as well as implement background downloads that can be both paused and resumed. By Felipe Laso-Marsetti.

4.6 (57) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Pausing, Resuming, and Canceling Downloads

What if the user wants to pause a download or to cancel it altogether? In this section, you’ll implement the pause, resume and cancel features to give the user complete control over the download process.

You’ll start by allowing the user to cancel an active download.

Canceling Downloads

In DownloadService.swift, add the following code inside cancelDownload(_:):

guard let download = activeDownloads[track.previewURL] else {
  return
}

download.task?.cancel()
activeDownloads[track.previewURL] = nil

To cancel a download, you’ll retrieve the download task from the corresponding Download in the dictionary of active downloads and call cancel() on it to cancel the task. You’ll then remove the download object from the dictionary of active downloads.

Pausing Downloads

Your next task is to let your users pause their downloads and come back to them later.

Pausing a download is similar to canceling it. Pausing cancels the download task, but also produces resume data, which contains enough information to resume the download at a later time if the host server supports that functionality.

Note: You can only resume a download under certain conditions. For instance, the resource must not have changed since you first requested it. For a full list of conditions, check out the documentation here.

Replace the contents of pauseDownload(_:) with the following code:

guard
  let download = activeDownloads[track.previewURL],
  download.isDownloading 
  else {
    return
}

download.task?.cancel(byProducingResumeData: { data in
  download.resumeData = data
})

download.isDownloading = false

The key difference here is that you call cancel(byProducingResumeData:) instead of cancel(). You provide a closure parameter to this method, which lets you save the resume data to the appropriate Download for future resumption.

You also set the isDownloading property of the Download to false to indicate that the user has paused the download.

Now that the pause function is complete, the next order of business is to allow the user to resume a paused download.

Resuming Downloads

Replace the content of resumeDownload(_:) with the following code:

guard let download = activeDownloads[track.previewURL] else {
  return
}

if let resumeData = download.resumeData {
  download.task = downloadsSession.downloadTask(withResumeData: resumeData)
} else {
  download.task = downloadsSession
    .downloadTask(with: download.track.previewURL)
}

download.task?.resume()
download.isDownloading = true

When the user resumes a download, you check the appropriate Download for the presence of resume data. If found, you’ll create a new download task by invoking downloadTask(withResumeData:) with the resume data. If the resume data is absent for any reason, you’ll create a new download task with the download URL.

In either case, you’ll start the task by calling resume and set the isDownloading flag of the Download to true to indicate the download has resumed.

Showing and Hiding the Pause/Resume and Cancel Buttons

There’s only one item left to do for these three functions to work: You need to show or hide the Pause/Resume and Cancel buttons, as appropriate.

To do this, TrackCell‘s configure(track:downloaded:) needs to know if the track has an active download and whether it’s currently downloading.

In TrackCell.swift, change configure(track:downloaded:) to configure(track:downloaded:download:):

func configure(track: Track, downloaded: Bool, download: Download?) {

In SearchViewController.swift, fix the call in tableView(_:cellForRowAt:):

cell.configure(track: track,
               downloaded: track.downloaded,
               download: downloadService.activeDownloads[track.previewURL])

Here, you extract the track’s download object from activeDownloads.

Back in TrackCell.swift, locate // TODO 14 in configure(track:downloaded:download:) and add the following property:

var showDownloadControls = false

Then replace // TODO 15 with the following:

if let download = download {
  showDownloadControls = true
  let title = download.isDownloading ? "Pause" : "Resume"
  pauseButton.setTitle(title, for: .normal)
}

As the comment notes, a non-nil download object means a download is in progress, so the cell should show the download controls: Pause/Resume and Cancel. Since the pause and resume functions share the same button, you’ll toggle the button between the two states, as appropriate.

Below this if-closure, add the following code:

pauseButton.isHidden = !showDownloadControls
cancelButton.isHidden = !showDownloadControls

Here, you show the buttons for a cell only if a download is active.

Finally, replace the last line of this method:

downloadButton.isHidden = downloaded

with the following code:

downloadButton.isHidden = downloaded || showDownloadControls

Here, you tell the cell to hide the Download button if the track is downloading.

Build and run your project. Download a few tracks concurrently and you’ll be able to pause, resume and cancel them at will:

Half Tunes App Screen With Pause, Resume, and Cancel Options

Showing Download Progress

At this point, the app is functional, but it doesn’t show the progress of the download. To improve the user experience, you’ll change your app to listen for download progress events and display the progress in the cells. There’s a session delegate method that’s perfect for this job!

First, in TrackCell.swift, replace // TODO 16 with the following helper method:

func updateDisplay(progress: Float, totalSize : String) {
  progressView.progress = progress
  progressLabel.text = String(format: "%.1f%% of %@", progress * 100, totalSize)
}

The track cell has progressView and progressLabel outlets. The delegate method will call this helper method to set their values.

Next, in SearchViewController.swift, add the following delegate method to the URLSessionDownloadDelegate extension:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                  didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
                  totalBytesExpectedToWrite: Int64) {
  // 1
  guard
    let url = downloadTask.originalRequest?.url,
    let download = downloadService.activeDownloads[url]  
    else {
      return
  }
  // 2
  download.progress = 
    Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
  // 3
  let totalSize = 
    ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, 
                              countStyle: .file) 
  // 4
  DispatchQueue.main.async {
    if let trackCell = 
      self.tableView.cellForRow(at: IndexPath(row: download.track.index,
                                              section: 0)) as? TrackCell {
      trackCell.updateDisplay(progress: download.progress, 
                              totalSize: totalSize)
    }
  }
}

Looking through this delegate method, step-by-step:

  1. You extract the URL of the provided downloadTask and use it to find the matching Download in your dictionary of active downloads.
  2. The method also provides the total bytes you have written and the total bytes you expect to write. You calculate the progress as the ratio of these two values and save the result in Download. The track cell will use this value to update the progress view.
  3. ByteCountFormatter takes a byte value and generates a human-readable string showing the total download file size. You’ll use this string to show the size of the download alongside the percentage complete.
  4. Finally, you find the cell responsible for displaying the Track and call the cell’s helper method to update its progress view and progress label with the values derived from the previous steps. This involves the UI, so you do it on the main queue.