APP前后台切换后,后台下载进度的UI更新失效问题求助
APP前后台切换后,后台下载进度的UI更新失效问题求助
我现在遇到一个头疼的问题:用这段代码实现文件下载并在CollectionView单元格里显示进度,但当APP在下载时被切到后台,再切回前台后,单元格里的进度标签就停更了,可实际上下载还在后台正常进行。有没有大佬能教教我怎么在APP回到前台后,让UI重新跟上下载进度呀?
import UIKit enum DownloadStatus { case none case inProgress case completed case failed } struct item { var number: Int! var downloadStatus: DownloadStatus = .none var progress: Float = 0.0 init(number: Int) { self.number = number } } var downloadQueue = [Int: [Int]]() var masterIndex = 0 extension URLSession { func getSessionDescription () -> Int { return Int(self.sessionDescription!)! } // Item ID func getDebugDescription () -> Int { return Int(self.debugDescription)! } // Collection ID } class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate { static var shared = DownloadManager() var identifier : Int = -1 var collectionId : Int = -1 var folderPath : String = "" typealias ProgressHandler = (Int, Int, Float) -> () var onProgress : ProgressHandler? { didSet { if onProgress != nil { let _ = activate() } } } override init() { super.init() } func activate() -> URLSession { let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background.\(NSUUID.init())") let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue()) urlSession.sessionDescription = String(identifier) urlSession.accessibilityHint = String(collectionId) return urlSession } private func calculateProgress(session : URLSession, completionHandler : @escaping (Int, 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 } }) completionHandler(session.getSessionDescription(), Int(session.accessibilityHint!)!, progress.reduce(0.0, +)) } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){ let stringNumb = (session.accessibilityHint ?? "hit") let someNumb = Int(stringNumb as String) let string1 = (session.sessionDescription ?? "hit") let some1 = Int(string1 as String) if let idx = downloadQueue[someNumb!]?.firstIndex(of: some1!) { downloadQueue[someNumb!]?.remove(at: idx) print("remove:\(downloadQueue)") } let fileName = downloadTask.originalRequest?.url?.lastPathComponent let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) let documentDirectoryPath:String = path[0] let fileManager = FileManager() var destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appending("/\(folderPath)")) do { try fileManager.createDirectory(at: destinationURLForFile, withIntermediateDirectories: true, attributes: nil) destinationURLForFile.appendPathComponent(String(describing: fileName!)) try fileManager.moveItem(at: location, to: destinationURLForFile) } catch (let error) { print(error) } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { if totalBytesExpectedToWrite > 0 { if let onProgress = onProgress { calculateProgress(session: session, completionHandler: onProgress) } } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { let stringNumb = (session.accessibilityHint ?? "hit") let someNumb = Int(stringNumb as String) let string1 = (session.sessionDescription ?? "hit") let some1 = Int(string1 as String) if let idx = downloadQueue[someNumb!]?.firstIndex(of: some1!) { downloadQueue[someNumb!]?.remove(at: idx) print("remove:\(downloadQueue)") } } } public struct Item: Decodable, Hashable { let index: Int let title: String let image: String let backgroundColor: String let borderColor: String } public struct Section: Decodable, Hashable { let index: Int let identifier: String let title: String let subtitle: String let item: [Item] } class CollectionController: UIViewController, UICollectionViewDelegate { typealias ProgressHandler = (Int, Float) -> () var onProgress : ProgressHandler? var items = [item]() var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<Section, Item>? let sections: [Section] = [.init(index: 0, identifier: "carouselCell", title: "title", subtitle: "sub", item: [ Item(index: 0, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 1, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 2, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 3, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 4, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 5, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 6, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 7, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 8, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 9, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 10, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 11, title: "Hello", image: "", backgroundColor: "", borderColor: ""), Item(index: 12, title: "Hello", image: "", backgroundColor: "", borderColor: "") ])] override func viewDidLoad() { super.viewDidLoad() createCollectionView() setupScrollView() let count = dataSource!.snapshot().numberOfItems for index in 0...count { items.append(item(number: index)) } } func setupScrollView() { collectionView.collectionViewLayout = createCompositionalLayout() collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false) } func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { [weak self] collectionView, indexPath, item in guard let self = self else { return UICollectionViewCell() } switch self.sections[indexPath.section].identifier { case "carouselCell": let cell = self.configure(CarouselCell.self, with: item, for: indexPath) if indexPath.row < self.items.count { // <- update UI base on self.items if self.items[indexPath.row].downloadStatus == .inProgress { cell.label.text = "\(String(format: "%.f%%", self.items[indexPath.row].progress * 100))" } } return cell default: return self.configure(CarouselCell.self, with: item, for: indexPath) } } } func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with item: Item, for indexPath: IndexPath) -> T { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError(" — \(cellType)") } cell.configure(with: item) return cell } func reloadData() { var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() snapshot.appendSections(sections) for section in sections { snapshot.appendItems(section.item, toSection: section) } dataSource?.apply(snapshot) } func createCollectionView() { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout()) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.contentInsetAdjustmentBehavior = .never view.addSubview(collectionView) collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier) createDataSource() reloadData() } func createCompositionalLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupWidth = (layoutEnvironment.container.contentSize.width * 1.05)/3 let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .absolute(groupWidth)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: (layoutEnvironment.container.contentSize.height/2) - (groupWidth/2), leading: 0, bottom: 0, trailing: 0) section.interGroupSpacing = 20 section.orthogonalScrollingBehavior = .groupPagingCentered return section } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let directory: String = path[0] let fileManager = FileManager() let destination = URL(fileURLWithPath: directory.appendingFormat("/\(indexPath.row+1)")) var queueArray = downloadQueue[indexPath.row+1] ?? [Int]() queueArray.append(indexPath.row+1) downloadQueue[indexPath.row+1] = queueArray let url = URL(string: "https://file-examples.com/storage/fe91352fe66730de9982024/2017/04/file_example_MP4_480_1_5MG.mp4")! let downloadManager = DownloadManager() downloadManager.identifier = indexPath.row+1 downloadManager.collectionId = indexPath.row+1 downloadManager.folderPath = "\(indexPath.row+1)" let downloadTaskLocal = downloadManager.activate().downloadTask(with: url) downloadTaskLocal.resume() //var item = items[indexPath.row] <- only update local value downloadManager.onProgress = { (row, tableId, progress) in let row = row - 1 //print("downloadManager.onProgress:\(row), \(tableId), \(String(format: "%.f%%", progress * 100))") DispatchQueue.main.async { if progress <= 1.0 { self.items[row].progress = progress if progress == 1.0 { self.items[row].downloadStatus = .completed } else { //cell.title.text = "\(String(format: "%.f%%", progress * 100))" self.items[row].downloadStatus = .inProgress } self.reloadItem(indexPath: .init(row: row, section: 0)) } } } } func reloadItem(indexPath: IndexPath) { guard let needReloadItem = dataSource!.itemIdentifier(for: indexPath) else { return } var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() snapshot.appendSections(sections) for section in sections { snapshot.appendItems(section.item, toSection: section) } dataSource?.apply(snapshot) snapshot.reloadItems([needReloadItem]) // <- reload items dataSource?.apply(snapshot, animatingDifferences: false) } } protocol SelfConfiguringCell: UICollectionViewCell { static var reuseIdentifier: String { get } func configure(with: Item) } class CarouselCell: UICollectionViewCell, SelfConfiguringCell { static var reuseIdentifier: String { "carouselCell" } let label = UILabel() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(label) contentView.backgroundColor = .green label.translatesAutoresizingMaskIntoConstraints = false label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true } func configure(with item: Item) { } required init?(coder: NSCoder) { fatalError("zzzzz") } }
问题根源分析
看了你的代码,核心问题出在后台URLSession的代理回调和UI更新的绑定关系上:
- 当APP进入后台时,前台创建的
onProgress闭包会因为页面生命周期或回调链路断开,导致后台下载的进度无法同步到UI; - 你为每个下载任务创建新的后台Session,但APP切回前台后,旧Session的回调没有重新关联到当前的UI控制器,进度事件无法传递到CollectionView;
- 另外,
reloadItem方法的快照更新逻辑冗余,且没有在APP回到前台时主动刷新所有正在下载的任务进度。
解决方案步骤
1. 重构DownloadManager,支持全局进度回调与Session管理
修改DownloadManager,让它能统一管理所有后台Session,并支持APP前后台切换时的回调恢复:
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate { static var shared = DownloadManager() // 保存所有活跃的后台Session private var activeSessions = [URLSession]() // 全局进度回调:key为itemId,支持多页面监听 private var globalProgressHandlers = [Int: ProgressHandler]() typealias ProgressHandler = (Int, Int, Float) -> () // ... 保留原有代码,修改以下部分 ... func activate() -> URLSession { let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background.item-\(identifier)") // 允许后台Session在前台触发回调 config.isDiscretionary = false config.sessionSendsLaunchEvents = true let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue()) urlSession.sessionDescription = String(identifier) urlSession.accessibilityHint = String(collectionId) activeSessions.append(urlSession) return urlSession } // 注册指定item的进度回调 func registerProgressHandler(for itemId: Int, handler: @escaping ProgressHandler) { globalProgressHandlers[itemId] = handler // 立即同步一次当前进度 syncCurrentProgress(for: itemId) } // 主动同步指定item的下载进度 private func syncCurrentProgress(for itemId: Int) { for session in activeSessions { guard let sessionItemId = Int(session.sessionDescription ?? "") else { continue } if sessionItemId == itemId { calculateProgress(session: session) { [weak self] (itemId, collectionId, progress) in self?.globalProgressHandlers[itemId]?(itemId, collectionId, progress) } break } } } // 统一触发进度更新 private func triggerProgressUpdate(session: URLSession, itemId: Int, collectionId: Int, progress: Float) { globalProgressHandlers[itemId]?(itemId, collectionId, progress) onProgress?(itemId, collectionId, progress) } // 重
相关产品推荐
相关产品推荐

