最近需要在VisionPro使用Swift UI和Swift进行视频的下载,存储和显示视频下载进度条,使用ChatGPT实现了一个基础的版本,看似没有问题,但是实际跑起来,debug用了好长时间。这个也是目前ChatGPT的利与弊吧。以下我分了两个版本介绍一下,如何进行文件下载和进度条同步。
基础版
声明URLSession
let session = URLSession(configuration: sessionConfig, delegate: URLSessionDelegateHandler)
获取session.downloadTask
downloadTask = session.downloadTask(with: url) { // completion handler code....}downloadTask.resume()
设置URLSessionDelegateHandler
class URLSessionDelegateHandler: NSObject, URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { } var progressHandler: (Float) -> Void var finishedHandler: (URL) -> Void init(progressHandler: @escaping (Float) -> Void, finishedHandler: @escaping (URL) -> Void) { self.progressHandler = progressHandler self.finishedHandler = finishedHandler } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) progressHandler(progress) }}
用以上几个方法就可以完成下载并监控进度了,但是当时遇到一个ChatGPT给我挖的坑是,进度条没有更新。后来进一步定位是,URLSessionDelegateHandler 相关的方法都没有被调用。费尽千辛万苦查到一个帖子:
stackoverflow.com/questions/6…
downloadTask的completion handler 优先级高于 delegate. 所以这里只要移除 downloadTask的completion handler 即可。
进阶版
参考:
matteomanferdini.com/swift-urlse…
定义一个 Download类
class Download: NSObject, URLSessionTaskDelegate { let url: URL let downloadSession: URLSession private var continuation: AsyncStream<Download.Event>.Continuation? var cancelHandler: (Data?) -> Void private lazy var task: URLSessionDownloadTask = { // Aliyun OSS sign URL let aliyunUrl = AliyunOSS.getSignedUrl(url.lastPathComponent)! let task = downloadSession.downloadTask(with: aliyunUrl) task.delegate = self return task }() init(url: URL, downloadSession: URLSession) { self.url = url self.downloadSession = downloadSession self.cancelHandler = {data in } } var isDownloading: Bool { task.state == .running } var events: AsyncStream<Download.Event> { AsyncStream { continuation in self.continuation = continuation task.resume() continuation.onTermination = { @Sendable [weak self] _ in self?.task.cancel() } } } func pause() { task.suspend() } func resume() { task.resume() } func cancel(cancelHandler: @escaping (Data?) -> Void) { task.cancel(byProducingResumeData: cancelHandler) }}extension Download { enum Event { case progress(currentBytes: Int64, totalBytes: Int64) case success(url: URL) }}extension Download: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { continuation?.yield( .progress( currentBytes: totalBytesWritten, totalBytes: totalBytesExpectedToWrite) ) } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { continuation?.yield(.success(url: newURL!)) continuation?.finish() } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { if error != nil { print("download error (String(describing: error))") } }}
然后在 View中,关联Download类,用来更新进度条
func downloadVideoV2(_ index: Int) async throws { let downloadResource = resource let url = URL(string: downloadResource.resource.cdn_url)! guard downloads[index] == nil else { return } let download = Download(url: url, downloadSession: model.downloadSession) downloads[index] = download for await event in download.events { self.process(event, for: downloadResource) } downloads[index] = nil}
func process(_ event: Download.Event, for downloadResource: DownloadVideoResource) { switch event { case let .progress(current, total): resource.update(currentBytes: current, totalBytes: total) case let .success(url): saveFile(for: downloadResource, at: url) return }}
但是遇到了一个特别难搞的问题,就是在下载结束后,success事件中,我要对临时文件进行移动到App的文件夹时,报错没有找到文件。报错内容如下:
"The file “CFNetworkDownload_l4WWyT.tmp” couldn’t be opened because there is no such file."
后来查阅到官方文档,
发现,只要这个delegate方法返回之后,这个文件就会被删除。原作者的意思是希望在主线程中完成文件的拷贝,但是,我一直没有实验成功。所以我的做法是,在这个delegate当中,完成moveItem操作,并将已经移动到Documents文件夹中的文件地址返回。
后续
文中还提到了使用 Aliyun OSS 签名URL防止盗链,后续会讲一下实现原理