I'm struggling to understand why the async-await version of URLSession download task APIs do not call the delegate functions, whereas the old non-async version that returns a reference to the download task works just fine.
Here is my sample code:
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
// This only prints the percentage of the download progress.
let calculatedProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let formatter = NumberFormatter()
formatter.numberStyle = .percent
print(formatter.string(from: NSNumber(value: calculatedProgress))!)
}
}
// Here's the VC.
final class DownloadsViewController: UIViewController {
private let url = URL(string: "https://pixabay.com/get/g0b9fa2936ff6a5078ea607398665e8151fc0c10df7db5c093e543314b883755ecd43eda2b7b5178a7e613a35541be6486885fb4a55d0777ba949aedccc807d8c_1280.jpg")!
private let delegate = DownloadDelegate()
private lazy var session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
// for the async-await version
private var task: Task<Void, Never>?
// for the old version
private var downloadTask: URLSessionDownloadTask?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
task?.cancel()
task = nil
task = Task {
let (_, _) = try! await session.download(for: URLRequest(url: url))
self.task = nil
}
// If I uncomment this, the progress listener delegate function above is called.
// downloadTask?.cancel()
// downloadTask = nil
// downloadTask = session.downloadTask(with: URLRequest(url: url))
// downloadTask?.resume()
}
}
What am I missing here?
I find this so bizarre since I am actually providing a delegate and I don't even have a way to provide a completion handler when calling the async function.
No, that makes sense. The async support is built on top of the completion handler support, aka the convenience methods. Given that background sessions don’t support the completion handler methods, they don’t support the async methods either.
As to the bigger picture, it’s a tricky one. When you use the convenience methods the system suppresses various associated callbacks. For example, when you call -downloadTaskWithURL:completionHandler:
(using Objective-C names because they are clearer) you don’t receive the -URLSession:downloadTask:didFinishDownloadingToURL:
and -URLSession:task:didCompleteWithError:
delegate callbacks because those are subsumed by the completion handler. And, as the async methods are based on the convenience methods, the fact that those delegate callbacks are suppressed there makes sense.
However, I’m surprised that connection:didWriteData:totalBytesWritten:expectedTotalBytes:
is not being delivered. I can’t see any good reason for it to be suppressed when you use the convenience methods. Feel free to file a bug about that. And, if you do, please post your bug number.
Still, this explains the issue you’re having with the async method, because it inherits its behaviour from the convenience one.
Supplying a per-request delegate won’t help here because it’s a URLSessionTaskDelegate
parameter and the callback you need is in the URLSessionDownloadDelegate
protocol. Again, that seems bugworthy to me. It’d make sense for the download async methods to take a URLSessionDownloadDelegate
parameter.
With the convenience methods the easiest way to get progress is via the task.progress
property. That’s not an option for the async method because you never get the task value. However, there is a sneaky workaround, namely to implement the -URLSession:didCreateTask:
method. This is delivered in the async case — regardless of whether you use a session delegate or a per-task delegate — and you can use it to set up a KVO observation on the progress.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"