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

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更新的绑定关系上:

  1. 当APP进入后台时,前台创建的onProgress闭包会因为页面生命周期或回调链路断开,导致后台下载的进度无法同步到UI;
  2. 你为每个下载任务创建新的后台Session,但APP切回前台后,旧Session的回调没有重新关联到当前的UI控制器,进度事件无法传递到CollectionView;
  3. 另外,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)
    }
    
    // 重
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 10:04:33