Search code examples
iosswiftuikituicollectionviewcompositionallayoutnsdiffabledatasourcesnapshot

How to fix freezes while collection view item update?


I have UICollectionViewCompositionalLayout. In didSelectItemAt I start downloading the file. Inside the collection view items I have a label showing the percentage of the file downloaded. I use this code for this and to update my cell:

func createDataSource() {
    dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
        switch self.sections[indexPath.section].identifier {

        case "cell":

            let cell = self.configure(DCell.self, with: item, for: indexPath)

            if indexPath.row < self.items.count {
                let item = self.items[indexPath.row]
                switch item.state {
                case .downloading: cell.title.text = "\(String(format: "%.f%%", item.progress * 100))"
                case .completed: cell.title.text = "Completed"
                case .failed: cell.title.text = "Fail"
                case .none: break
                }
            }
            return cell

        default: return self.configure(DCell.self, with: item, for: indexPath)
        }
    }
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    // activate the download manager code...
    let url = URL(string: "http")!
    let downloadManager = DownloadManager()
    downloadManager.identifier = indexPath.row+1
    downloadManager.collectionId = 0
    downloadManager.folderPath = "\(indexPath.row+1)"
    let downloadTaskLocal = downloadManager.activate().downloadTask(with: url)
    downloadTaskLocal.resume()

    // get progress:
    downloadManager.onProgress = { [weak self] (row, collection, progress) in
        guard let self = self else { return }
        DispatchQueue.main.async {
            self.items[row - 1].progress = progress
            switch progress {
                case 1.0: self.items[row - 1].state = .completed
                case _: self.items[row - 1].state = .downloading
            }
            self.reloadItem(indexPath: .init(row: row - 1, 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])
    dataSource?.apply(snapshot, animatingDifferences: false)
}

code from downloadManager to get progress:

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

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

But when my file is downloading and I use this method reloadItem(indexPath: IndexPath) to update a cell, I am getting some freezes when scrolling the collection view and freezes if I click again on the cell that currently show the downloading progress. My problem is inside this method reloadItem(indexPath: IndexPath). But how to fix this problem with cell update?

for @dezinezync

using your code the scroll view freeze was fixed. But if I click again on the cell that currently show the downloading progress I have strange behaviour. In cell class I have this code to show animation:

class DCell: UICollectionViewCell, SelfConfiguringCell {

    private func animateScale(to scale: CGFloat, duration: TimeInterval) {
        UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [], animations: {
            self.stackView.transform = .init(scaleX: scale, y: scale)
            self.textView.transform = .init(scaleX: scale, y: scale)
        }, completion: nil)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        animateScale(to: 0.9, duration: 0.4)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        animateScale(to: 1, duration: 0.38)
        isSelected.toggle()
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event)
        animateScale(to: 1, duration: 0.38)
        isSelected.toggle()
    }

}

But if I click again on the cell that currently show the downloading progress my cell sticks. It becomes smaller than others. And when clicked the animation will not appear. didSelectItemAt not called. But for other cells the animation works fine before the progress starts showing. I don't understand why this happens and didSelectItemAt not called


Solution

  • Please try the following change:

    
    func reloadItem(indexPath: IndexPath) {
        guard let needReloadItem = dataSource!.itemIdentifier(for: indexPath) else { return }
            
        guard var snapshot = dataSource?.snapshot() else { return } 
            
        snapshot.reloadItems([needReloadItem])
        dataSource?.apply(snapshot, animatingDifferences: false)
    }
    

    Once you form your initial snapshot, subsequent changes should typically refer to the same snapshot. This ensures that the DataSource can perform efficient diffs between the snapshot it currently holds and the newly applied one.