Swift4中URL数组循环下载PDF及后台下载进度追踪问题
解决PDF后台下载+跨页面进度追踪的方案
你的递归下载思路方向是对的,但问题出在下载任务和ViewController绑定太紧密,导致VC销毁后任务追踪丢失,而且没利用Alamofire的后台会话能力。下面给你一套完整的实现方案:
第一步:抽离独立的下载任务管理器(单例)
把下载逻辑从ViewController里抽出来,做成单例类,这样不管页面怎么切换,任务都能在后台持续运行,而且所有页面都能访问任务状态。
import Alamofire import Combine class PDFDownloadManager { static let shared = PDFDownloadManager() // 后台会话配置 private let session: Session = { let configuration = URLSessionConfiguration.background(withIdentifier: "com.yourApp.pdfDownloadSession") configuration.isDiscretionary = false // 非 discretionary 任务会立即启动 configuration.sessionSendsLaunchEvents = true // 后台完成时唤醒App return Session(configuration: configuration) }() // 存储下载任务信息:key是任务ID,value是进度、状态、目标URL等 private var downloadTasks: [String: DownloadTaskInfo] = [:] // 待下载的URL队列 private var pendingURLs: [URL] = [] // 当前正在运行的任务 private var currentTask: DownloadRequest? // 用Combine发布进度更新,方便页面订阅 let progressSubject = PassthroughSubject<[String: DownloadTaskInfo], Never>() // 下载任务信息结构体 struct DownloadTaskInfo { let url: URL var progress: Double var status: DownloadStatus var localPath: URL? } enum DownloadStatus: String { case waiting, downloading, completed, failed } private init() { // 监听后台会话完成回调 session.backgroundCompletionHandler = { [weak self] in self?.progressSubject.send(self?.downloadTasks ?? [:]) } } // 添加下载任务(支持批量添加) func addDownloadURLs(_ urls: [URL]) { pendingURLs.append(contentsOf: urls) // 如果当前没有任务,启动第一个 if currentTask == nil { startNextTask() } progressSubject.send(downloadTasks) } // 启动下一个任务 private func startNextTask() { guard let url = pendingURLs.popLast() else { // 队列空了,任务全部完成 currentTask = nil progressSubject.send(downloadTasks) return } // 生成唯一任务ID(用URL的hash或者UUID) let taskID = url.absoluteString // 初始化任务信息 downloadTasks[taskID] = DownloadTaskInfo(url: url, progress: 0, status: .downloading, localPath: nil) progressSubject.send(downloadTasks) // 创建下载请求 currentTask = session.download(url) .downloadProgress { [weak self] progress in guard let self = self else { return } self.downloadTasks[taskID]?.progress = progress.fractionCompleted self.progressSubject.send(self.downloadTasks) } .responseData { [weak self] response in guard let self = self else { return } switch response.result { case .success(let data): // 保存PDF到本地 let localPath = self.savePDF(data, from: url) self.downloadTasks[taskID]?.status = .completed self.downloadTasks[taskID]?.localPath = localPath case .failure(let error): print("下载失败:\(error.localizedDescription)") self.downloadTasks[taskID]?.status = .failed } // 当前任务完成,启动下一个 self.startNextTask() } } // 保存PDF到Documents目录 private func savePDF(_ data: Data, from url: URL) -> URL? { let fileName = url.lastPathComponent guard let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil } let localPath = documentsDir.appendingPathComponent(fileName) do { try data.write(to: localPath) return localPath } catch { print("保存失败:\(error.localizedDescription)") return nil } } // 获取所有任务状态 func getAllTaskInfos() -> [String: DownloadTaskInfo] { return downloadTasks } }
第二步:配置后台会话回调(AppDelegate)
要让App在后台完成下载后能更新状态,需要在AppDelegate里处理后台会话的回调:
import UIKit import Alamofire @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { // 把completionHandler传给下载管理器 PDFDownloadManager.shared.session.setBackgroundCompletionHandler(completionHandler) } }
第三步:在ViewController中订阅进度更新
不管是下载VC还是主表单VC,都可以通过Combine来获取实时进度:
下载ViewController示例
import UIKit import Combine class DownloadViewController: UIViewController { @IBOutlet weak var progressTableView: UITableView! private var cancellables = Set<AnyCancellable>() private var taskInfos: [PDFDownloadManager.DownloadTaskInfo] = [] override func viewDidLoad() { super.viewDidLoad() setupProgressSubscription() progressTableView.reloadData() } private func setupProgressSubscription() { PDFDownloadManager.shared.progressSubject .receive(on: DispatchQueue.main) .sink { [weak self] taskDict in self?.taskInfos = Array(taskDict.values) self?.progressTableView.reloadData() } .store(in: &cancellables) // 初始化获取当前任务状态 let initialTasks = PDFDownloadManager.shared.getAllTaskInfos() taskInfos = Array(initialTasks.values) } } // 实现TableView数据源,显示每个任务的进度和状态 extension DownloadViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return taskInfos.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ProgressCell", for: indexPath) let taskInfo = taskInfos[indexPath.row] cell.textLabel?.text = taskInfo.url.lastPathComponent cell.detailTextLabel?.text = "\(Int(taskInfo.progress * 100))% - \(taskInfo.status.rawValue)" // 这里可以添加UIProgressView显示具体进度条 return cell } }
主表单ViewController示例
在主表单里可以添加按钮打开下载VC,或者直接显示总进度:
import UIKit import Combine class MainFormViewController: UIViewController { @IBOutlet weak var downloadStatusLabel: UILabel! private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() setupStatusSubscription() } private func setupStatusSubscription() { PDFDownloadManager.shared.progressSubject .receive(on: DispatchQueue.main) .sink { [weak self] taskDict in let completedCount = taskDict.values.filter { $0.status == .completed }.count let totalCount = taskDict.count self?.downloadStatusLabel.text = "已完成 \(completedCount)/\(totalCount) 个PDF" } .store(in: &cancellables) } @IBAction func openDownloadVC(_ sender: UIButton) { let downloadVC = storyboard?.instantiateViewController(withIdentifier: "DownloadViewController") as! DownloadViewController present(downloadVC, animated: true) } // 触发下载的方法 @IBAction func startDownload(_ sender: UIButton) { let pdfURLs = [ URL(string: "https://example.com/pdf1.pdf")!, URL(string: "https://example.com/pdf2.pdf")! // 更多URL... ] PDFDownloadManager.shared.addDownloadURLs(pdfURLs) } }
关键要点说明
- 单例管理器:解耦下载逻辑和UI,保证任务在后台持续运行,不受ViewController生命周期影响。
- 后台会话:Alamofire的background session由系统托管下载任务,即使App退到后台也能继续执行。
- 进度同步:用Combine发布进度事件,所有需要显示进度的页面都能订阅,实时更新UI。
- 任务追踪:每个下载任务都有唯一ID和状态记录,就算关闭下载VC再打开,也能恢复显示所有任务的进度和状态。
这样改造后,你就能实现后台下载、跨页面查看进度的需求了,而且任务追踪的问题也彻底解决,每个任务的状态都持久保存在管理器中,随时可以获取。
内容的提问来源于stack exchange,提问作者Anonymous-E
相关产品推荐
相关产品推荐

