如何用新的尝试await URLSession.shared.download(...)获取下载进度

问题描述 投票:0回答:2

Apple 刚刚引入了 async/await 和一堆使用它们的

Foundation
函数。我正在使用新的异步/等待模式下载文件,但我似乎无法获取下载进度。

(downloadedURL, response) = try await URLSession.shared.download(for: dataRequest, delegate: self) as (URL, URLResponse)

如您所见,有一个委托,我尝试使我的类符合

URLSessionDownloadDelegate
并实现
urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) 
函数,但它永远不会被调用。

我还尝试创建一个新的 URLSession 并将其委托设置为同一个类,希望 URLSession 能够调用此函数,但它永远不会被调用,并且文件仍然可以愉快地下载。但我需要进度,请问如何获得?

swift async-await progress urlsession
2个回答
17
投票

一些观察:

  • delegate
    中的
    download(for:delegate:)
    URLSessionTaskDelegate
    ,而不是
    URLSessionDownloadDelegate
    ,因此不能保证会调用特定于下载的委托方法。

  • FWIW,在 使用 async/await 与 URLSession 中,他们说明委托用于身份验证质询,而不是用于下载进度。

  • 使用传统的

    URLSessionTask
    方法,如果您调用
    downloadTask(with:completionHandler:)
    ,则不会调用下载进度,而仅当您在没有完成处理程序的情况下调用再现时,
    downloadTask(with:)
    。正如从网站下载文件所说:

    如果您想在下载过程中接收进度更新,则必须使用委托。

    如果新的

    download(for:delegate:)
    在幕后使用
    downloadTask(with:completionHandler:)
    ,人们很容易想象为什么人们可能看不到报告的下载进度。

但所有这些都是学术性的。最重要的是,您看不到使用

download(for:delegate:)
download(from:delegate:)
报告的进度。因此,如果您想在下载过程中看到进度,您有几个选择:

  1. 按照 Won

     的建议实施 
    bytes(from:),并在字节进入时更新进度。

    顺便说一句,我可能建议将其流式传输到文件(例如,

    OutputStream

    )而不是将其附加到
    Data
    ,以反映下载任务的内存特征。但是,他的回答说明了基本想法。

  2. 回到基于委托的

    downloadTask(with:)

    解决方案。


如果您想编写自己的报告进度的版本,您可以执行以下操作:

extension URLSession { func download(from url: URL, delegate: URLSessionTaskDelegate? = nil, progress parent: Progress) async throws -> (URL, URLResponse) { try await download(for: URLRequest(url: url), progress: parent) } func download(for request: URLRequest, delegate: URLSessionTaskDelegate? = nil, progress parent: Progress) async throws -> (URL, URLResponse) { let progress = Progress() parent.addChild(progress, withPendingUnitCount: 1) let bufferSize = 65_536 let estimatedSize: Int64 = 1_000_000 let (asyncBytes, response) = try await bytes(for: request, delegate: delegate) let expectedLength = response.expectedContentLength // note, if server cannot provide expectedContentLength, this will be -1 progress.totalUnitCount = expectedLength > 0 ? expectedLength : estimatedSize let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent(UUID().uuidString) guard let output = OutputStream(url: fileURL, append: false) else { throw URLError(.cannotOpenFile) } output.open() var buffer = Data() if expectedLength > 0 { buffer.reserveCapacity(min(bufferSize, Int(expectedLength))) } else { buffer.reserveCapacity(bufferSize) } var count: Int64 = 0 for try await byte in asyncBytes { try Task.checkCancellation() count += 1 buffer.append(byte) if buffer.count >= bufferSize { try output.write(buffer) buffer.removeAll(keepingCapacity: true) if expectedLength < 0 || count > expectedLength { progress.totalUnitCount = count + estimatedSize } progress.completedUnitCount = count } } if !buffer.isEmpty { try output.write(buffer) } output.close() progress.totalUnitCount = count progress.completedUnitCount = count return (fileURL, response) } }
与:

extension OutputStream { /// Write `Data` to `OutputStream` /// /// - parameter data: The `Data` to write. func write(_ data: Data) throws { try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { throw OutputStreamError.bufferFailure } var bytesRemaining = buffer.count while bytesRemaining > 0 { let bytesWritten = write(pointer, maxLength: bytesRemaining) if bytesWritten < 0 { throw OutputStreamError.writeFailure } bytesRemaining -= bytesWritten pointer += bytesWritten } } } }
注:

  1. 这使用一个小缓冲区来避免尝试一次将整个资产加载到内存中。它将结果写入文件中。

    如果资产可能很大(这通常就是我们使用

    download

     而不是 
    data
     的原因),这一点很重要。

  2. 请注意,

    expectedContentLength

    有时可以是-1,在这种情况下我们不知道正在下载的文件的大小。上面的内容处理了这种情况。

    在资产规模未知的情况下估算进度的逻辑是个人偏好的问题。上面我使用了估计的资产规模并调整了进度。它不会非常准确,但至少它反映了下载过程中的

    一些进度。

  3. 我添加了

    try Task.checkCancellation()

    ,以便可以取消下载任务。

  4. 我使用

    Progress

    向家长报告进度。您可以根据需要挂钩并显示它,但如果您使用 
    UIProgressView
    ,则特别简单。

无论如何,你可以做这样的事情:

func startDownloads(_ urls: [URL]) async throws { let cachesFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let progress = Progress() progressView.observedProgress = progress // assuming you are just updating a `UIProgressView` with the overall progress of all the downloads try await withThrowingTaskGroup(of: Void.self) { group in progress.totalUnitCount = Int64(urls.count) for url in urls { group.addTask { let destination = cachesFolder.appendingPathComponent(url.lastPathComponent) // obviously, put the resulting file wherever you want let (url, _) = try await URLSession.shared.download(from: url, progress: progress) try? FileManager.default.removeItem(at: destination) try FileManager.default.moveItem(at: url, to: destination) } } try await group.waitForAll() } }
    

15
投票
您可以使用

URLSession.shared.bytes(from: imageURL)

for await in
 循环播放。

URLSession.shared.bytes

 返回 
(URLSession.AsyncBytes, URLResponse)
。 AsyncBytes 是一个异步序列,可以使用 
for await in
 进行循环。

func fetchImageInProgress(imageURL: URL) async -> UIImage? { do { let (asyncBytes, urlResponse) = try await URLSession.shared.bytes(from: imageURL) let length = (urlResponse.expectedContentLength) var data = Data() data.reserveCapacity(Int(length)) for try await byte in asyncBytes { data.append(byte) let progress = Double(data.count) / Double(length) print(progress) } return UIImage(data: data) } catch { return nil } }
它显示如下渐进式获取图像。

© www.soinside.com 2019 - 2024. All rights reserved.