Downloading Files in 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.

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

iOS has a great support for file downloads using the Foundation’s NSURLConnection. An NSURLConnection object lets you load the contents of a URL by providing a URL request object. The interface for NSURLConnection is sparse, providing only the controls to start and cancel asynchronous loads of a URL request.

Alamofire

Given the huge developer community, there are several tools and libraries already built around to make your life easier. Rather than reinventing the wheel with boilerplate around NSURLConnection, let’s try to set up downloads using Alamofire which greatly simplifies the download tasks.

From the documentation, it’s very simple to initiate file downloads using Alamofire using Alamofire.download. To select a download destination, we need to use the DownloadFileDestination block to return the file url download should be stored at. Finally, for listening to download progress updates, we can add the downloadProgress block. One thing to take care of in the progress block is that it is invoked on the background thread, so if you want to make some changes to the UI (e.g. updating a progress view), use DispatchQueue.main.async to execute the updates on UI thread.

Let’s put everything together:

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

   self.downloadProgressView?.setProgress(value: 0, animationDuration: 0)
   
   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])
   }

   downloadRequest = Alamofire.download(url, to:destination).downloadProgress {progress in
     DispatchQueue.main.async {
         self.downloadProgressView?.setProgress(value: CGFloat(progress.fractionCompleted * 100), animationDuration: 0.1)
     }
   }.response { defaultDownloadResponse in
     // TODO: Handle cancelled error
     if let error = defaultDownloadResponse.error {
         log.warning("Download Failed with error - \(error)")
         self.onError(.Download)
         return
     }
     guard let downloadedFilePath = self.downloadedFilePath else { return }
     log.debug("Downloaded file successfully to \(downloadedFilePath)")
     // TODO: Handle downloaded file
   }
 }

Common File Utilities

I often use common utility functions to access directory locations throughout the app (This is using Path from FileKit, so make sure you add it in Podfile or Cartfile if you plan to use these utilities too).

class Utils {
  internal static func tempDirectory() throws -> Path {
    return try self.directoryInsideDocumentsWithName(name: "temp")
  }

  internal static func directoryInsideDocumentsWithName(name: String, create: Bool = true) throws -> Path {
   let directory = Path(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]) + name
   if create && !directory.exists {
     try directory.createDirectory()
   }
   return directory
  }
}

That’s all you need to integrate a simple download component in your app. To learn more about downloading files, check out the next post that describes how to add support for pausing and resuming the downloads without redownloading the already downloaded content.

Published 18 Sep 2017

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