iosswiftuicollectionviewuikit

How to show progress in collection view item?


I'm using UICollectionViewCompositionalLayout with UICollectionViewCell class. Also I have a DownloadManager class. I want to download a file after clicking a cell. I'm using didSelectItemAt method to start downloading. And I'm using progressView and title in UICollectionViewCell class to show the progress state when downloading starts. But if I scroll my collection view when my file is in the process of downloading my cell downloading progress will jump over and show up in another cell that does not downloading. In other words, the progressView shows progress in different cells, but I'm only downloading one file. How to fix it?

enum DownloadStatus {
    case none
    case inProgress
    case completed
    case failed
}

struct item {
    var number: Int!
    var downloadStatus: DownloadStatus = .none
    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
}

DownloadManager

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!]?.index(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!]?.index(of: some1!) {
            downloadQueue[someNumb!]?.remove(at: idx)
            print("remove:\(downloadQueue)")
        }
    }
    
}

UICollectionView

class CollectionController: UIViewController, UICollectionViewDelegate {
    
    typealias ProgressHandler = (Int, Float) -> ()
    var onProgress : ProgressHandler?
    var items = [item]()
        
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
    let sections = Bundle.main.decode([Section].self, from: "carouselSection.json")
    
    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.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
    }
        
    func createDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
            switch self.sections[indexPath.section].identifier {
            case "carouselCell": return self.configure(CarouselCell.self, with: item, for: indexPath)
            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.isScrollEnabled = false
        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.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: "link")!
        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 cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "carouselCell", for: indexPath) as! CarouselCell
        cell = self.collectionView?.cellForItem(at: indexPath) as! CarouselCell
        
        var item = items[indexPath.row]
        
        downloadManager.onProgress = { (row, tableId, progress) in
            print("downloadManager.onProgress:\(row), \(tableId), \(String(format: "%.f%%", progress * 100))")
            DispatchQueue.main.async {
                if progress <= 1.0 {
                    cell.progressView.progress = progress
                    if progress == 1.0 {
                        item.downloadStatus = .completed
                    } else {
                        cell.title.text = "\(String(format: "%.f%%", progress * 100))"
                        item.downloadStatus = .inProgress
                    }
                }
            }
        }
        
    }
    
}

Update for new answers:

In my project I have Item to parse JSON and I use it in collection. And item from downloadManager

Item for parse JSON

public struct Item: Decodable, Hashable {
    let index: Int              
    let title: String           
    let image: String           
    let backgroundColor: String 
    let borderColor: String     
}

Section:

public struct Section: Decodable, Hashable {
    let index: Int         
    let identifier: String 
    let title: String      
    let subtitle: String   
    let item: [Item]       
}

item from DownloadManager

struct item {
    var number: Int!
    var downloadStatus: DownloadStatus = .none
    init(number: Int) { self.number = number }
}

CollectionView cell

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)
    }

Solution

  • As @Duncan C point out problems in your code. Here is my suggestion to refactor your code: CollectionController will interact to DownloadManager then store current progress to Item, Instead of create item to keep track of download progress, you can store download progress as local varialable in Item:

    struct Item: Decodable {
        let index: Int
        let title: String
        let image: String
        let backgroundColor: String
        let borderColor: String
        
        //local variable for Download task
        //need to set this value to optional for parsing JSON work properly
        var progress: CGFloat? = 0.0 // <- store progress
        var downloadStatus: DownloadStatus? = .none
    }
    
    extension Item: Hashable {
        func hash(into hasher: inout Hasher) {
            hasher.combine(index)
        }
    }
    
    enum DownloadStatus: String, Decodable {
        case none
        case inProgress
        case completed
        case failed
    }
    
    public struct Section: Decodable, Hashable {
        let index: Int
        let identifier: String
        let title: String
        let subtitle: String
        let item: [Item]
    }
    
    extension Item: Equatable {
        static func == (lhs: Item, rhs: Item) -> Bool {
            lhs.title == rhs.title && lhs.image == rhs.image && lhs.backgroundColor == rhs.backgroundColor &&
            lhs.index == rhs.index && lhs.borderColor == rhs.borderColor
        }
    }
    
    
    
    class CollectionController: UIViewController, UICollectionViewDelegate {
        var mapFromIndexToDownloadManager: [Item: DownloadManager] = [:] // <- cache current DownloadManager
        //...
        
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            //get Item
            let item: Item = ...
            if item.downloadStatus == .none || item.downloadStatus == .failed { // <- check to see if their has been an download progress for IndexPath before, also can check download status, ...
                
                // create DownloadManager
                mapFromIndexToDownloadManager[indexPath.row] = downloadManager // <- cache DownloadManager
                let downloadTaskLocal = downloadManager.activate().downloadTask(with: url)
                downloadTaskLocal.resume()
                downloadManager.onProgress = { [weak self] (row, tableId, progress) in
                    guard let self = self else { return }
                    var currItem = self.datasource.snapshot().itemIdentifiers
                    currItem[row].progress = progress
                    var snapshot = Snapshot()
                    snapshot.appendSections([.main])
                    snapshot.appendItems(currItem)
                    // apply new snapshot to reload item
                    datasource.apply(snapshot)
                }
            }
        }
    }
    

    Now in your cell, just set your progressView.progress base on progress property of 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)
        //set progress here
        if let progress = item.progress {
            cell.progressView.progress = progress
        }
    }
    

    Updated Answer: As Alex want to store download progress in seperated model from Item. Then when download progress got update, we have to manually reload items to update UI. Here is my minimal producuable 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() } }
        }
        
        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
                        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")
        }
    }