I was trying to backport URLSession's download(for:delegate:)
since it requires a deployment target of iOS 15+ but concurrency is now supported for iOS 13+.
It seemed like a very straight forward process since you can pass a continuation into the completion handler of downloadTask(with:completionHandler:)
.
This is what I came up with:
extension URLSession {
func asyncDownload(for request: URLRequest) async throws -> (URL, URLResponse) {
return try await withCheckedThrowingContinuation { continuation in
downloadTask(with: request) { url, response, error in
if let url = url, let response = response {
continuation.resume(returning: (url, response))
} else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
}
}.resume()
}
}
}
However there is a subtlety in the implementation of downloadTask(with:completionHandler:)
that keeps this code from working correctly. The file behind the URL is deleted right after returning the completion block. Therefore the file is not available anymore after the awaiting.
I could reproduce this by placing FileManager.default.fileExists(atPath: url.path)
right before resuming the continuation and the same line after awaiting this call. The first yielded a true
the second one a false
.
I then tried to use Apple's implementation download(for:delegate:)
which magically did not have the same problem I am describing. The file at the given URL was still available after awaiting.
A possible solution would be to move the file to a different location inside downloadTask
's closure. However in my opinion this is a separation-of-concern nightmare conflicts with the principle of separation-of-concern. I have to introduce the dependency of a FileManager
to this call, which Apple is (probably) not doing either since the URL returned by downloadTask(with:completionHandler:)
and download(for:delegate:)
looks identical.
So I was wondering if there is a better solution to wrapping this call and making it async other than I am doing right now. Maybe you could somehow keep the closure from returning until the Task
is finished? I'd like to keep the responsibility of moving the file to a better destination to the caller of asyncDownload(for:)
.