URLSessionDownloadTaskDelegate functions not called when using URLSession.download(for:), but works when using URLSession.downloadTask(with:)

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?

Note that the full signature for URLSession.download(for:) is actually this:

func download(for request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (URL, URLResponse)

So I’m thinking this means the async version ignores the session’s delegate and only uses the optional delegate you can pass in here.

Why ignore the session delegate? Just a guess, but the session delegate is documented as running on a specific OperationQueue that you specify when creating the session. But the async version doesn’t say what queue the delegate may run on. So this API design allows it to run on a different queue than OperationQueue used for the old-style tasks.

Thank you @Scott, but I'm afraid that supplying the delegate to URLSession.download(for:delegate:) does not work, just as it does not when supplying the delegate in the URLSession initializer.

At some point, I thought that maybe I needed to initialize the URLSession with a .background configuration instead of .default. However, when I did this, the line that calls URLSession.download(for:) throws an exception that exactly says: Completion handler blocks are not supported in background sessions. Use a delegate instead. 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.

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"

URLSessionDownloadTaskDelegate functions not called when using URLSession.download(for:), but works when using URLSession.downloadTask(with:)
 
 
Q