skip to Main Content

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

2

Answers


  1. You are doing a number of things incorrectly.

    • You should not be calling dequeueReusableCell in your didSelectItemAt method.

    • You should not be triggering a new download every time the user selects a cell. You need to check to see if the file has already been downloaded and use the local copy if so. (Downloading is slow!)

    • You should not be trying to update cell contents from your download manager’s onProgress method. Instead, you should modify your data model and tell the collection view that the cell needs updating. (Cells get when you scroll, and get reused to represent a different item in your collection. So if you scroll while a download is in progress, the cell the download manager is trying to update can be scrolled off-screen, tossed into the recycle bin, and then picked up and assigned to a different item.) This is likely the cause of the problem you describe.

    • You should not be calling reloadData() from your createCollectionView() method.

    Those are just the things I saw at a glance. Collection views and table views are complex beasts and don’t work the way you might think they do. I suggest studying some tutorials on creating them. We all get bitten by cell reuse bugs when we first start out with them.

    Login or Signup to reply.
  2. As @Duncan C point out problems in your code. Here is my suggestion to refactor your code:

    class CarouselCell: UICollectionViewCell {
        private var downloadManager: DownloadManager?
        
        override func prepareForReuse() {
            super.prepareForReuse()
            // <- reset onProgress closure of old downloadManager
            downloadManager?.onProgress = nil
        }
        
        func bind(downloadManager: DownloadManager?) {
            // setup UI base on downloadManager status
            self.downloadManager = downloadManager
            downloadManager.onProgress = { (row, tableId, progress) in
                //update cell UI here
                ...
            }
        }
    }
    
    class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
        var status: DownloadStatus = .none //<- save Download status here
        ...
        
        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)
            // update download status here
            if error == nil {
                status = .completed
            } else {
                status = .failed
            }
            
            if let idx = downloadQueue[someNumb!]?.firstIndex(of: some1!) {
                downloadQueue[someNumb!]?.remove(at: idx)
                print("remove:(downloadQueue)")
            }
        }
        
    }
    
    class CollectionController: UIViewController, UICollectionViewDelegate {
        
        var mapFromIndexToDownloadManager: [Int: DownloadManager] = [:] // <- cache current DownloadManager
        ...
        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)
                    cell.bind(downloadManager: self.mapFromIndexToDownloadManager[indexPath.row]
                    return cell
                    default:
                    return self.configure(CarouselCell.self, with: item, for: indexPath)
                }
            }
        }
        
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            if mapFromIndexToDownloadManager[indexPath.row] != nil { // <- check to see if there has been an download progress for IndexPath before, also can check download status, ...
                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)"
                mapFromIndexToDownloadManager[indexPath.row] = downloadManager // <- cache DownloadManager
                let downloadTaskLocal = downloadManager.activate().downloadTask(with: url)
                downloadTaskLocal.resume()
            }
            collectionView.reloadItems(at: [indexPath]) // <- reload cell here
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search