Swift中如何将NSURLSession多文件下载进度聚合为总进度?
解决NSURLSession多文件下载合并总进度的问题
这个问题我之前也碰到过,核心就是要统一统计所有下载任务的总字节数和已下载字节数,而不是单个任务的进度比例直接返回。下面给你一步步拆解实现方案:
核心思路
- 为每个下载任务单独记录「总预期下载字节数」和「已下载字节数」
- 维护全局的「总预期字节数」和「总已下载字节数」
- 总进度 = 总已下载字节数 / 总预期字节数(仅当总预期字节数>0时计算)
- 只有当所有任务都完成/失败,且总已下载字节数等于总预期字节数时,才会显示100%
修改后的DownloadManager实现
import Foundation // 存储单个下载任务的进度信息 struct DownloadTaskInfo { var totalBytesExpected: Int64 var bytesReceived: Int64 } class DownloadManager: NSObject, URLSessionDelegate, URLSessionDownloadDelegate { // 单例实例 static let shared = DownloadManager() // URLSession实例 private var session: URLSession! // 存储每个任务的进度信息,key为taskIdentifier private var tasksInfo: [Int: DownloadTaskInfo] = [:] // 全局总预期字节数 private var totalExpectedBytes: Int64 = 0 // 全局总已下载字节数 private var totalReceivedBytes: Int64 = 0 // 进度回调闭包,返回0.0~1.0的进度值 var progressHandler: ((Double) -> Void)? private override init() { super.init() // 初始化URLSession let config = URLSessionConfiguration.background(withIdentifier: "com.yourdomain.DownloadManager") session = URLSession(configuration: config, delegate: self, delegateQueue: nil) } // 添加下载任务的方法 func addDownloadTask(with url: URL) { let task = session.downloadTask(with: url) task.resume() } // MARK: - URLSessionDownloadDelegate func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { // 任务完成,更新总已下载字节数(确保和总预期一致) guard let taskInfo = tasksInfo[downloadTask.taskIdentifier] else { return } totalReceivedBytes += (taskInfo.totalBytesExpected - taskInfo.bytesReceived) // 移除已完成的任务信息 tasksInfo.removeValue(forKey: downloadTask.taskIdentifier) // 计算并回调最终进度 updateTotalProgress() } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let taskId = downloadTask.taskIdentifier // 首次获取任务总大小,初始化任务信息并累加总预期字节数 if tasksInfo[taskId] == nil { let taskInfo = DownloadTaskInfo(totalBytesExpected: totalBytesExpectedToWrite, bytesReceived: totalBytesWritten) tasksInfo[taskId] = taskInfo totalExpectedBytes += totalBytesExpectedToWrite } else { // 更新任务的已下载字节数 var taskInfo = tasksInfo[taskId]! taskInfo.bytesReceived = totalBytesWritten tasksInfo[taskId] = taskInfo // 更新全局总已下载字节数(这里要注意:首次初始化已经加过一次,后续只需要加本次写入的字节数) totalReceivedBytes += bytesWritten } // 计算并回调总进度 updateTotalProgress() } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { let taskId = task.taskIdentifier guard let taskInfo = tasksInfo[taskId] else { return } if let error = error { // 任务失败,移除任务信息,并调整总预期和已下载字节数 totalExpectedBytes -= taskInfo.totalBytesExpected totalReceivedBytes -= taskInfo.bytesReceived tasksInfo.removeValue(forKey: taskId) print("下载失败:\(error.localizedDescription)") } // 回调进度 updateTotalProgress() } // MARK: - 私有方法:计算并回调总进度 private func updateTotalProgress() { var progress: Double = 0.0 if totalExpectedBytes > 0 { progress = Double(totalReceivedBytes) / Double(totalExpectedBytes) // 确保进度不会超过1.0(比如网络波动导致的字节统计误差) progress = min(progress, 1.0) } // 回到主线程回调(因为代理方法在后台线程执行) DispatchQueue.main.async { self.progressHandler?(progress) } } }
关键代码解释
- DownloadTaskInfo结构体:专门存储单个任务的下载状态,避免全局变量混乱。
- tasksInfo字典:通过
taskIdentifier作为key,快速定位每个任务的信息。 - totalExpectedBytes & totalReceivedBytes:全局统计所有任务的总大小和已下载量,这两个值是计算总进度的核心。
- updateTotalProgress方法:统一处理进度计算和回调,确保在主线程更新UI(因为URLSession的代理方法默认在后台线程执行)。
- 错误处理:在
didCompleteWithError中处理任务失败的情况,及时从全局统计中移除失败任务的字节数,避免进度计算错误。
使用示例
// 初始化并设置进度回调 DownloadManager.shared.progressHandler = { progress in print("总下载进度:\(String(format: "%.2f%%", progress * 100))") // 更新UI,比如progressView.progress = progress } // 添加多个下载任务 if let url1 = URL(string: "https://example.com/file1.zip") { DownloadManager.shared.addDownloadTask(with: url1) } if let url2 = URL(string: "https://example.com/file2.zip") { DownloadManager.shared.addDownloadTask(with: url2) }
这样实现后,只有当所有下载任务都完成(没有失败)时,总进度才会达到100%;如果有任务失败,总进度会基于剩余完成的任务重新计算,不会出现单个任务完成就跳100%的情况。
内容的提问来源于stack exchange,提问作者Nuibb
相关产品推荐
相关产品推荐

