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

如何修复UICollectionView滚动时下载进度条显示错位的问题

如何修复UICollectionView滚动时下载进度条显示错位的问题

嗨,这个问题我太熟了!本质上是UICollectionView的cell复用机制在搞鬼——你直接持有了选中cell的引用,滚动时这个cell会被拿来展示其他数据项,进度就跟着“跑”到别的cell上了。咱们一步步来解决:


1. 把下载状态和进度绑定到数据模型上

你现在分开维护了Item(JSON解析用)和item(下载管理用)两个结构体,很容易搞混。直接把下载相关的属性整合到Item里,让每个展示的数据项都自带自己的下载状态:

public struct Item: Decodable, Hashable {
    let index: Int              
    let title: String           
    let image: String           
    let backgroundColor: String 
    let borderColor: String     
    // 新增下载相关属性
    var downloadStatus: DownloadStatus = .none
    var downloadProgress: Float = 0.0
}

这样就不用单独维护items数组了,直接用sections里的Item即可。

2. 让DownloadManager只传递关键信息,不直接操作UI

现在你的DownloadManager回调里有冗余信息,应该简化成传递数据项的标识(比如index)和当前进度,让Controller自己去找到对应的数据源更新:

class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
    static let shared = DownloadManager() // 确保用单例,不要每次创建新实例
    typealias ProgressHandler = (Int, Float) -> () // 传递item的index和进度
    var onProgress : ProgressHandler?
    
    // ... 其他原有代码不变,修改以下方法
    
    private func calculateProgress(session : URLSession, completionHandler : @escaping (Int, Float) -> ()) {
        session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
            let progress = downloads.map({ (task) -> Float in
                if task.countOfBytesExpectedToReceive > 0 {
                    return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
                } else {
                    return 0.0
                }
            }).reduce(0.0, +)
            let itemIndex = session.getSessionDescription()
            completionHandler(itemIndex, progress)
        }
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if totalBytesExpectedToWrite > 0 {
            onProgress.map { calculateProgress(session: session, completionHandler: $0) }
        }
    }
    
    // 下载完成时更新进度为100%,标记状态为完成
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
        let itemIndex = session.getSessionDescription()
        onProgress?(itemIndex, 1.0)
        // ... 原有文件保存逻辑不变
    }
    
    // 下载失败时标记状态为失败
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        let itemIndex = session.getSessionDescription()
        if error != nil {
            onProgress?(itemIndex, -1.0) // 用-1标记失败
        }
        // ... 原有队列清理逻辑不变
    }
}

3. 在CollectionController里,通过数据源更新UI,不直接持有cell

去掉原来的items数组,直接操作sections里的Item。启动下载时更新对应Item的状态,进度回调时更新数据源并刷新cell:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard var selectedItem = sections[indexPath.section].item[safe: indexPath.item] else { return }
    
    // 避免重复下载
    if selectedItem.downloadStatus == .inProgress { return }
    
    // 更新数据源状态,刷新当前cell
    selectedItem.downloadStatus = .inProgress
    sections[indexPath.section].item[indexPath.item] = selectedItem
    collectionView.reloadItems(at: [indexPath])
    
    // 启动下载
    let url = URL(string: "link")!
    let downloadManager = DownloadManager.shared
    downloadManager.identifier = selectedItem.index
    downloadManager.collectionId = indexPath.section
    downloadManager.folderPath = "\(selectedItem.index)"
    
    downloadManager.onProgress = { [weak self] itemIndex, progress in
        DispatchQueue.main.async {
            guard let self = self else { return }
            // 找到对应的Item并更新状态
            for sectionIndex in self.sections.indices {
                if let itemIndexInSection = self.sections[sectionIndex].item.firstIndex(where: { $0.index == itemIndex }) {
                    var updatedItem = self.sections[sectionIndex].item[itemIndexInSection]
                    switch progress {
                    case 1.0:
                        updatedItem.downloadStatus = .completed
                        updatedItem.downloadProgress = 1.0
                    case -1.0:
                        updatedItem.downloadStatus = .failed
                        updatedItem.downloadProgress = 0.0
                    default:
                        updatedItem.downloadStatus = .inProgress
                        updatedItem.downloadProgress = progress
                    }
                    self.sections[sectionIndex].item[itemIndexInSection] = updatedItem
                    // 刷新对应的cell
                    let cellIndexPath = IndexPath(item: itemIndexInSection, section: sectionIndex)
                    self.collectionView.reloadItems(at: [cellIndexPath])
                }
            }
        }
    }
    
    let downloadTaskLocal = downloadManager.activate().downloadTask(with: url)
    downloadTaskLocal.resume()
}

4. 在Cell的configure方法里,根据Item的状态和进度更新UI

这样每次cell被复用的时候,都会根据当前绑定的Item数据正确显示状态:

func configure(with item: Item) {
    title.text = item.title
    textView.backgroundColor = UIColor(item.backgroundColor)
    textView.layer.borderColor = UIColor(item.borderColor).cgColor
    progressView.layer.borderColor = UIColor(item.borderColor).cgColor
    imageView.image = UIImage(named: item.image)
    
    // 根据下载状态更新UI
    switch item.downloadStatus {
    case .none:
        progressView.progress = 0.0
        title.text = item.title // 显示原标题
    case .inProgress:
        progressView.progress = item.downloadProgress
        title.text = "\(String(format: "%.f%%", item.downloadProgress * 100))"
    case .completed:
        progressView.progress = 1.0
        title.text = "已完成"
    case .failed:
        progressView.progress = 0.0
        title.text = "下载失败"
    }
}

核心思路总结

所有的状态都存在数据源里,cell只是负责根据数据渲染UI,永远不要直接持有cell引用做UI更新——这样不管cell怎么复用,都能正确对应到当前的数据项,进度条就不会“乱跑”了。

备注:内容来源于stack exchange,提问作者Alex Smith

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.16 02:54:49