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 能够调用此函数,但它永远不会被调用,并且文件仍然可以愉快地下载。但我需要进度,请问如何获得?
一些观察:
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:)
报告的进度。因此,如果您想在下载过程中看到进度,您有几个选择:
按照 Won
的建议实施
bytes(from:)
,并在字节进入时更新进度。
顺便说一句,我可能建议将其流式传输到文件(例如,OutputStream
)而不是将其附加到
Data
,以反映下载任务的内存特征。但是,他的回答说明了基本想法。
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
}
}
}
}
注:
如果资产可能很大(这通常就是我们使用
download
而不是
data
的原因),这一点很重要。
expectedContentLength
有时可以是-1,在这种情况下我们不知道正在下载的文件的大小。上面的内容处理了这种情况。在资产规模未知的情况下估算进度的逻辑是个人偏好的问题。上面我使用了估计的资产规模并调整了进度。它不会非常准确,但至少它反映了下载过程中的
一些进度。
try Task.checkCancellation()
,以便可以取消下载任务。
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()
}
}
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
}
}
它显示如下渐进式获取图像。