【问题标题】:How to get the download progress with the new try await URLSession.shared.download(...)如何通过新尝试获取下载进度 await URLSession.shared.download(...)
【发布时间】:2021-07-06 20:18:31
【问题描述】:

Apple 刚刚引入了 async/await 和一堆使用它们的 Foundation 函数。我正在使用新的 async/await 模式下载文件,但我似乎无法获得下载进度。

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

如您所见,有一个委托,我尝试让我的类符合 URLSessionDownloadDelegate 并实现 urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) 函数,但它从未被调用。

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

【问题讨论】:

  • 我遇到了同样的问题。我的猜测是这还没有实现,因为 Swift 5.5 仍处于测试阶段。我建议向 Apple 发送反馈。

标签: swift async-await


【解决方案1】:

您可以使用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
    }
}

如下图所示渐进式抓取图片。

【讨论】:

    【解决方案2】:

    一些观察:

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

    • FWIW,在 Use async/await with URLSession 中,它们说明委托用于身份验证挑战,而不是用于下载进度。

    • 使用传统的URLSessionTask 方法,如果您调用downloadTask(with:completionHandler:),则不会调用下载进度,而是仅在您调用没有完成处理程序downloadTask(with:) 的再现时才调用。正如Downloading Files from Websites 所说:

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

      如果新的download(for:delegate:) 在幕后使用downloadTask(with:completionHandler:),不难想象为什么看不到下载进度报告。

    但所有这些都是学术性的。最重要的是,您看不到使用download(for:delegate:)download(from:delegate:) 报告的进度。因此,如果您想在下载过程中看到进度,您有以下几种选择:

    1. bytes(from:) 实现为suggested by Won 并在字节进入时更新您的进度。

      顺便说一句,我可能会建议将其流式传输到文件(例如,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()
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2013-08-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-26
      • 1970-01-01
      • 1970-01-01
      • 2013-02-07
      相关资源
      最近更新 更多