Episode #240

File Downloads - Part 2

Series: Large File Downloads

32 minutes
Published on October 20, 2016

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

In this episode we create a DownloadInfo model in CoreData in order to track the state of a download, separate from any view controller.

Episode Links

Creating a Model to track Download State

import Foundation
import CoreData

enum DownloadStatus : String {
    case Pending
    case Downloading
    case Paused
    case Failed
    case Completed
}

@objc(DownloadInfo)
class DownloadInfo : NSManagedObject {
    var status: DownloadStatus? {
        get {
            guard let value = statusValue else { return nil }
            return DownloadStatus(rawValue: value)
        }
        set {
            statusValue = status?.rawValue
        }
    }

    static var offlineLocation: URL {
        let docsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        return URL(fileURLWithPath: docsDir).appendingPathComponent(".downloads", isDirectory: true)
    }
}

Create the model in a Pending State

class DownloadController {

    // ...

    func download(episode: Episode) {
        guard let videoURL = episode.videoURL else { return }

        let context = PersistenceManager.sharedContainer.viewContext
        let downloadInfo = DownloadInfo(context: context)
        downloadInfo.downloadedAt = NSDate()
        downloadInfo.status = .Pending
        downloadInfo.episode = episode
        downloadInfo.progress = 0
        try! context.save()

        let operation = DownloadOperation(url: videoURL, episodeID: Int(episode.id))
        downloadQueue.addOperation(operation)
    }
}

Updating the model during the download

class DownloadOperation {

    // ...

   lazy var downloadInfo: DownloadInfo! = {
        let fetchRequest: NSFetchRequest<DownloadInfo> = DownloadInfo.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "episode.id = %d", self.episodeID)
        fetchRequest.fetchLimit = 1
        return try! self.context.fetch(fetchRequest).first!
    }()

    override func execute() {
        downloadInfo.status = .Downloading
        try! PersistenceManager.save(context: context)
        downloadTask = session.downloadTask(with: url)
        downloadTask?.resume()
    }

   // ...

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {

        // ...        

        downloadInfo.progress = progress
        downloadInfo.sizeInBytes = totalBytesWritten

        // ...

    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("Did complete download")

        if let e = error as? NSError {
            print("Error: \(e)")

            if e.domain == NSURLErrorDomain && e.code == NSURLErrorCancelled {
                downloadInfo.status = .Paused
            } else {
                downloadInfo.status = .Failed
            }
        } else {
            downloadInfo.status = .Completed
        }
        try! PersistenceManager.save(context: context)

        finish()
    }

   // ...
}

Moving the downloaded file to a permanent location

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("File downloaded to: \(location)")

        let ext = downloadTask.currentRequest?.url?.pathExtension ?? "mp4"
        let uuid = UUID()
        let dir = DownloadInfo.offlineLocation

        if !FileManager.default.fileExists(atPath: dir.path) {
            try! FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false, attributes: nil)
        }

        let filename = "\(uuid.uuidString).\(ext)"
        let targetLocation = dir.appendingPathComponent(filename)

        try! FileManager.default.moveItem(at: location, to: targetLocation)
        let attribs = try! FileManager.default.attributesOfItem(atPath: targetLocation.path)

        downloadInfo.progress = 1.0

        if let sizeNumber = attribs[FileAttributeKey.size] as? NSNumber {
            downloadInfo.sizeInBytes = sizeNumber.int64Value
        }

        downloadInfo.path = targetLocation.path
    }