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