iosswiftuicollectionviewurlsession

UI errors with downloading files


I'm using this code to download my files and display progress in a my collection view item. But if I hide the app while some items are downloading and after open app again, the progress in the item label will stop, but it won't stop downloading. How to update the user interface after the my app opens again?

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].state == .downloading {
                    cell.title.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")
    }
}

Solution

  • In your code, you are not update downloadStatus of item when download task finish or throwing error.

    To fix your problem, I suggest to add 2 closure to notify when download success or error. Here is my example:

    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() } }
        }
        var onSuccess: ((Int) -> Void)? // <- get called when download success
        var onError: ((Error?, Int) -> Void)? // <- get called when download error
        
        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:: did finish :: \(downloadQueue)")
            }
            onSuccess?(collectionId) // call when download success
             
            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)
            onError?(error, collectionId) // call when download error
            if let idx = downloadQueue[someNumb!]?.firstIndex(of: some1!) {
                downloadQueue[someNumb!]?.remove(at: idx)
                print("remove when complete:\(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
                        let item = self.items[indexPath.row]
                        switch item.downloadStatus {
                        case .inProgress:
                            cell.label.text = "\(String(format: "%.f%%", self.items[indexPath.row].progress * 100))"
                        case .completed:
                            cell.label.text = "Completed"
                        case .failed:
                            cell.label.text = "FAIL"
                        case .none:
                            break
                        }
                    }
                    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]
            
            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/fec85039006734629a992d7/2017/04/file_example_MP4_640_3MG.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.onError = { [weak self] error, row in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    // change download status to .failed when download error
                    self.items[row - 1].downloadStatus = .failed
                    self.reloadItem(indexPath: .init(row: row - 1, section: 0))
                }
                
            }
            downloadManager.onSuccess = { [weak self] row in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    // change download status to .completed when download success
                    self.items[row - 1].downloadStatus = .completed
                    self.reloadItem(indexPath: .init(row: row - 1, section: 0))
                }
            }
            downloadManager.onProgress = { [weak self] (row, tableId, progress) in
                guard let self = self else { return }
                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")
        }
    }
    

    Also as I debug, method func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) is not get called when app is in background.