You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS开发:在UITableViewCell中实现多文件并行下载及进度更新

Fixing Multi-File Parallel Downloads in UITableView

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 URLSession and download tasks directly in UITableViewCell causes 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.pdf name, so later downloads overwrite earlier ones
  • Type casting self as! URLSessionDelegate is 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: prepareForReuse resets state, and progress updates target the correct cell using data source indexes
  • Memory Safety: Uses weak self in closures to avoid retain cycles

内容的提问来源于stack exchange,提问作者Ankit Prajapati

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:19:46