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
You are doing a number of things incorrectly.
You should not be calling
dequeueReusableCell
in yourdidSelectItemAt
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 yourcreateCollectionView()
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.
As @Duncan C point out problems in your code. Here is my suggestion to refactor your code: