VisionPro开发 - 文件下载与进度条

133 阅读2分钟

最近需要在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) -> Voidinit(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防止盗链,后续会讲一下实现原理