Resuming downloads on iOS

Introduction

With the increasing amount of data being used by mobile apps, it’s becoming common for most apps to download data to support offline functions. The downloaded data could be for additional textures for supporting complex games, ebooks for readers or music/video files for video/music players or companion guide apps that are becoming the de-facto standard for every big tourist attraction.

In the previous post, I described how to add simple file downloads in your app in less than 100 lines of code. Most often, the apps implementing their own download managers forget one very important aspect of this process. Pausing and resuming downloads. As the sizes of download packages increase, it becomes even more important to allow users to pause the download and resume later when they have better internet connectivity.

Downloading files on iOS

For the complete code to download files on iOS, check out the previous post. We get a downloadRequest when initiating a download with Alamofire. In order to allow users to cancel the download, all we need is to cancel the request using downloadRequest.cancel() when the user presses Cancel.

Save resume data

When a download is cancelled (either on user’s request or due to a network or internal error), Alamofire invokes the response handler where we can handle the response error and process resume data. Neither iOS nor Alamofire, automatically save this resume data to resume the download later. In order to support resuming downloads, the first step is to save the resume data in the cache so that this can be used later when initiating the download again.

Let’s see how to handle resume data in response block from Alamofire.

if let error = defaultDownloadResponse.error {
  log.warning("Download Failed with error - \(error)")
  self.onError(.Download)
  if let resumeData = defaultDownloadResponse.resumeData {
    Shared.dataCache.set(value: resumeData, key: self.url)
  }
  return
}

We will be using data cache from Haneke in this post to store the resume data. If you are using another library or have rolled out your own cache implementation, you need to change the occurrences of Shared.dataCache to fit your implementation.

Resume download from saved data

The next step is to provide this resume data when trying to resume the download.

First, fetch the resume data from the cache.

Shared.dataCache.fetch(key: url).onSuccess {resumeData in
  self.startDownload(with: resumeData)
}.onFailure {_ in
  self.startDownload()
}

Now provide this data when initiating the download. Alamofire makes this really simple. All we need is to add the additional resumingWith param when creating the request.

Alamofire.download(resumingWith: resumeData, to: destination)

Putting everything together

Let’s put everything together into our shiny new beginDownload method.

internal func beginDownload() {
  UIApplication.shared.isIdleTimerDisabled = true

  self.downloadProgressView?.startProgress(to: 0, duration: 0)

  if SYSTEM_VERSION_LESS_THAN(version: "10.0") || SYSTEM_VERSION_GREATER_THAN(version: "10.2") {
    Shared.dataCache.fetch(key: url).onSuccess {resumeData in
      self.startDownload(with: resumeData)
      }.onFailure {_ in
        self.startDownload()
    }
  } else {
    self.startDownload()
  }
}

private func startDownload(with resumeData: Data? = nil) {
  let destination: DownloadRequest.DownloadFileDestination = {temporaryURL, response in
    if let suggestedFileName = response.suggestedFilename {
      do {
        let directory = try Utils.tempDirectory()
        self.downloadedFilePath = (directory + suggestedFileName)
        if let downloadedFilePath = self.downloadedFilePath {
          if downloadedFilePath.exists { try self.downloadedFilePath?.deleteFile() }
          return (URL(fileURLWithPath: downloadedFilePath.rawValue), [.removePreviousFile, .createIntermediateDirectories])
        }
      } catch let e {
        log.warning("Failed to get temporary directory - \(e)")
      }
    }

    let (downloadedFileURL, _) = DownloadRequest.suggestedDownloadDestination()(temporaryURL, response)
    self.downloadedFilePath = Path(downloadedFileURL.absoluteString)
    return (downloadedFileURL, [.removePreviousFile, .createIntermediateDirectories])
  }

  if let resumeData = resumeData {
    downloadRequest = Alamofire.download(resumingWith: resumeData, to: destination)
  } else {
    downloadRequest = Alamofire.download(url, to:destination)
  }

  downloadRequest?.downloadProgress {progress in
    DispatchQueue.main.async {
      self.downloadProgressView?.startProgress(to: CGFloat(progress.fractionCompleted * 100), duration: 0.1)
    }
  }.response { defaultDownloadResponse in
      // TODO Handle cancelled error
      if let error = defaultDownloadResponse.error {
        log.warning("Download Failed with error - \(error)")
        self.onError(.Download)
        if let resumeData = defaultDownloadResponse.resumeData  {
          Shared.dataCache.set(value: resumeData, key: self.url)
        }
        return
      }
      guard let downloadedFilePath = self.downloadedFilePath else { return }
      log.debug("Downloaded file successfully to \(downloadedFilePath)")
      self.unzipFile(at: downloadedFilePath)
  }
}

That’s all you need for supporting pause and resume on your iOS apps. The next time you are integrating download in an app, check out this post and do a favor to your users by integrating the resume function.

Published 28 Oct 2017

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter