如何修复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
相关产品推荐
相关产品推荐

