iOS开发:在UITableViewCell中实现多文件并行下载及进度更新
Hey there! Let's fix up your UITableView file download implementation step by step. Your current code has a few key issues that'll cause bugs like cell reuse glitches, overwritten downloaded files, and inefficient session management. Here's a structured solution to get parallel downloads working smoothly with proper UI updates:
First, Let's Identify the Core Issues in Your Current Code
- Storing
URLSessionand download tasks directly inUITableViewCellcauses problems when cells are reused (progress gets lost or attached to the wrong cell) - Each cell creates its own background session, which is inefficient and violates iOS best practices
- All downloads save to the same
file.pdfname, so later downloads overwrite earlier ones - Type casting
self as! URLSessionDelegateis unsafe and risks crashes - No proper cleanup or state reset when cells are reused
Step 1: Move Download Logic to a ViewModel
Let's create a dedicated DownloadViewModel to handle all download tasks, progress tracking, and file management. This separates business logic from UI, avoids reuse issues, and lets us manage parallel downloads centrally.
import UIKit class DownloadViewModel: NSObject, URLSessionDownloadDelegate { // Track active tasks and their progress by file URL private var activeTasks: [URL: URLSessionDownloadTask] = [:] var progressTracking: [URL: Float] = [:] // Closures to notify UI of updates var onProgressUpdated: ((URL, Float) -> Void)? var onDownloadFinished: ((URL, Bool, Error?) -> Void)? // Single background session (best practice for background downloads) private lazy var downloadSession: URLSession = { let config = URLSessionConfiguration.background(withIdentifier: "lanet.PDFDownload") config.isDiscretionary = false config.sessionSendsLaunchEvents = true return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue()) }() // Start a download for a given URL func startDownload(for url: URL) { guard activeTasks[url] == nil else { return } // Prevent duplicate downloads let task = downloadSession.downloadTask(with: url) activeTasks[url] = task progressTracking[url] = 0.0 task.resume() } // MARK: - URLSessionDownloadDelegate func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { guard let fileURL = downloadTask.originalRequest?.url else { return } let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) progressTracking[fileURL] = progress // Update UI on main thread DispatchQueue.main.async { self.onProgressUpdated?(fileURL, progress) } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { guard let fileURL = downloadTask.originalRequest?.url else { onDownloadFinished?(URL(fileURLWithPath: ""), false, NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid file URL"])) return } // Save file with unique name (using the URL's last path component) let fileName = fileURL.lastPathComponent let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationURL = documentsDir.appendingPathComponent(fileName) do { // Replace existing file if it exists if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: location, to: destinationURL) onDownloadFinished?(fileURL, true, nil) } catch { onDownloadFinished?(fileURL, false, error) } // Clean up completed task activeTasks.removeValue(forKey: fileURL) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let fileURL = task.originalRequest?.url else { return } onDownloadFinished?(fileURL, false, error) activeTasks.removeValue(forKey: fileURL) } }
Step 2: Refactor the UITableViewCell for UI Only
Your cell should only handle displaying data and triggering actions—no download logic. We'll add a filename label and use closures to communicate with the ViewModel.
class PDFCell: UITableViewCell { @IBOutlet weak var btnStartDownload: UIButton! @IBOutlet weak var downloadProgress: UIProgressView! @IBOutlet weak var fileNameLabel: UILabel! // Add this label in your xib/storyboard var fileURL: URL? var downloadTappedHandler: ((URL) -> Void)? override func prepareForReuse() { super.prepareForReuse() // Reset state when cell is reused downloadProgress.progress = 0.0 btnStartDownload.isEnabled = true btnStartDownload.setTitle("下载", for: .normal) fileURL = nil fileNameLabel.text = nil } // Update progress UI safely func updateDownloadProgress(_ progress: Float) { downloadProgress.progress = progress if progress >= 1.0 { btnStartDownload.setTitle("已完成", for: .normal) btnStartDownload.isEnabled = false } } @IBAction func btnDownloadClick(_ sender: Any) { guard let url = fileURL else { return } btnStartDownload.setTitle("下载中", for: .normal) btnStartDownload.isEnabled = false downloadTappedHandler?(url) } }
Step 3: Coordinate Everything in Your ViewController
The ViewController will manage the table view, data source, and connect the ViewModel to the cells.
class DownloadViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! // Sample data: replace with your actual file URLs and names private let fileDataSource: [(url: URL, displayName: String)] = [ (URL(string: "https://example.com/report1.pdf")!, "季度报告1.pdf"), (URL(string: "https://example.com/manual.pdf")!, "用户手册.pdf"), (URL(string: "https://example.com/dataset.pdf")!, "数据集.pdf") ] private let downloadViewModel = DownloadViewModel() override func viewDidLoad() { super.viewDidLoad() tableView.register(UINib(nibName: "PDFCell", bundle: nil), forCellReuseIdentifier: "PDFCell") setupViewModelCallbacks() } private func setupViewModelCallbacks() { // Update cell progress when download progresses downloadViewModel.onProgressUpdated = { [weak self] url, progress in guard let self = self, let index = self.fileDataSource.firstIndex(where: { $0.url == url }), let cell = self.tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? PDFCell else { return } cell.updateDownloadProgress(progress) } // Update cell state when download finishes (success/failure) downloadViewModel.onDownloadFinished = { [weak self] url, success, error in guard let self = self, let index = self.fileDataSource.firstIndex(where: { $0.url == url }), let cell = self.tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? PDFCell else { return } if success { cell.btnStartDownload.setTitle("已完成", for: .normal) } else { cell.btnStartDownload.setTitle("下载失败,重试", for: .normal) cell.btnStartDownload.isEnabled = true print("Download error for \(url): \(error?.localizedDescription ?? "Unknown error")") } } } // MARK: - TableView DataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fileDataSource.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "PDFCell", for: indexPath) as! PDFCell let file = fileDataSource[indexPath.row] cell.fileNameLabel.text = file.displayName cell.fileURL = file.url // Restore progress if download is already in progress if let progress = downloadViewModel.progressTracking[file.url] { cell.updateDownloadProgress(progress) cell.btnStartDownload.setTitle(progress >= 1.0 ? "已完成" : "下载中", for: .normal) cell.btnStartDownload.isEnabled = progress < 1.0 } // Connect download tap handler cell.downloadTappedHandler = { [weak self] url in self?.downloadViewModel.startDownload(for: url) } return cell } }
Key Improvements Summarized
- Separation of Concerns: Download logic lives in a ViewModel, cells handle only UI, ViewController coordinates everything (follows MVC principles)
- Parallel Downloads: The single background session manages multiple tasks efficiently, allowing parallel downloads
- No File Overwrites: Files are saved with their original filenames from the URL
- Cell Reuse Safety:
prepareForReuseresets state, and progress updates target the correct cell using data source indexes - Memory Safety: Uses
weak selfin closures to avoid retain cycles
内容的提问来源于stack exchange,提问作者Ankit Prajapati

