Episode #239

File Downloads - Part 1

Series: Large File Downloads

30 minutes
Published on October 6, 2016

This video is only available to subscribers. Get access to this video and 582 others.

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

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)

        ...        
    }