Downloading large files on iOS represents some unique challenges. Downloads should occur in the background, not confined to a particular view controller. They should be able to report progress on multiple screens, and should be robust enough to survive application suspension and failing network conditions, and respect the user's cellular data plan. In this episode we start a series on downloading large files that will cover all of the above concerns.
This episode utilizes Swift 3 and Xcode 8. Episode Links Source Code Decoupled from View Controllers File downloads should be decoupled from view controllers, so the user is free to navigate around while the file is being downloaded. To accomplish this, we will do our download in an Operation: class DownloadOperation : BaseOperation, URLSessionDownloadDelegate { let url: URL let episodeID: Int let sessionConfiguration = URLSessionConfiguration.default lazy var session: URLSession = { let session = URLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue: nil) return session }() var downloadTask: URLSessionDownloadTask? init(url: URL, episodeID: Int) { self.url = url self.episodeID = episodeID } override func execute() { downloadTask = session.downloadTask(with: url) downloadTask?.resume() } override func cancel() { downloadTask?.cancel() } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) let userInfo: [String: Any] = [ "episodeID" : episodeID, "progress" : progress, "totalBytesWritten" : totalBytesWritten, "totalBytesExpectedToWrite" : totalBytesExpectedToWrite ] DispatchQueue.main.async { NotificationCenter.default.post(name: .downloadProgress, object: self, userInfo: userInfo) } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { print("Did complete. error? \(error)") finish() } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { print("File downloaded to: \(location)") } } Now the question becomes, where do we enqueue this operation? Who will own the queue?. I decided to create a DownloadController that we can access from multiple places to maintain the queue: class DownloadController { let downloadQueue: OperationQueue static let shared = DownloadController() private init() { downloadQueue = OperationQueue() downloadQueue.maxConcurrentOperationCount = 1 } func download(episode: Episode) { guard let videoURL = episode.videoURL else { return } let operation = DownloadOperation(url: videoURL, episodeID: Int(episode.id)) downloadQueue.addOperation(operation) } } With that in place, now we can kick off the download and report on progress in our view controller: func viewDidLoad() { ... NotificationCenter.default.addObserver(self, selector: #selector(EpisodeViewController.onDownloadProgress(notification:)), name: .downloadProgress, object: nil) } func onDownloadProgress(notification: Notification) { guard let progress = notification.userInfo?["progress"] as? Float, let id = notification.userInfo?["episodeID"] as? Int, id == Int(episode.id) else { return } let formattedProgress = String(format: "%.1f%%", progress * 100.0) progressLabel.text = formattedProgress progressView.progress = progress } @IBAction func downloadTapped(_ sender: AnyObject) { DownloadController.shared.download(episode: episode) ... }