swiftswiftuiurlsession

Tracking progress for multi-file downloads using URLSessionDownloadTask(s)


I'm working on a project that requires me to track download progress for multiple files (as one).

For now I've implemented this class to represent a download:

class Download {
    var isActive: Bool
    var progress: Float
    var resumeData: Data?
    var task: URLSessionDownloadTask?
    var object: MyObject
}

I use an ObservableObject that is injected into the environment to handle downloads:

class DownlaodTaskHandler: NSObject, URLSessionDownloadDelegate, ObservableObject {
    @Published var activeTasks: [URL:Download]

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // get Download by downloadTask.originalRequest?.url, mark as finished, move file etc.
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        // get Download by downloadTask.originalRequest?.url, update progress
    }
}

This works pretty well for single files, but I haven't been able to find a way to efficiently track progress for downloads made up of multiple files.

I though about using MyObject.id and something like

let tasksForObject = activeTasks.values.filter { dl in
    dl.object.id == currentDownload.object.id
}

var progress: Float = 0
for task in tasksForObject {
    progress += task.progress
}
progress = progress / Float(tasks.ForObject.count)

but this feels like it's really inefficient and would cause unnecessarily high CPU load, especially as the delegate method is called thousands of times per download.

I'm pretty sure that I'm just stuck going in the completely wrong direction and missing the obvious solution.


Solution

  • Just like I suspected, I missed an obvious solution. I've ended up using this:

    .onReceive(Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()) { _ in
                let taskForBook = downloadTaskHandler.activeTasks.values.filter { dl in
                    dl.audiobook.id == audiobook.id
                }
                
                var progress: Float = 0
                
                for task in taskForBook {
                    progress += task.progress
                }
                let prg = progress / Float(taskForBook.count)
                    DispatchQueue.main.async {
                        self.progress = progress / Float(taskForBook.count)
                    }
            }
    

    But on the view observing the download status instead of the session delegate. There's probably a better way to do this, but this works and is fairly lightweight.