1

I am trying to display download progress in my collectionview cells. Im currently using the parse progressblock which has an instance of the cell and updates the progress bar.

}, progressBlock: { (percent) in
    self.mainQueue.addOperation {

    // set the downloadProgess var to update from cellForItemAt
    // downloadProgress = (Float(percent) / Float(100))

    if let downloadingCell = self.collectionView.cellForItem(at: self.indexPath) as? InnerCollectionCell {
        downloadingCell.progressBar.isHidden = false
        downloadingCell.contentView.bringSubview(toFront: downloadingCell.progressBar)
        downloadingCell.progressBar.setProgress(Float(percent) / Float(100), animated: true)
        downloadingCell.setNeedsDisplay()
        downloadingCell.setNeedsLayout()
        downloadingCell.isUserInteractionEnabled = false
        downloadingCell.spinner.isHidden = true
    }
}
})

So this works fine, the problem i now have is if i leave this view controller then come back to see how the downloads are going the instance of the cell has been reused and none of the desired UI elements are visible but the progress is still ticking away in the background.

The only place i can think to re-display the UI elements is in cellForItemAt. The problem then is that the progress doesn't update, it just shows the value at the time the cell was reloaded.

How can i go about reusing the instance of the cell that the progress block is using or cleanly displaying ui elements that continue to update?

Pippo
  • 1,439
  • 1
  • 18
  • 35
  • Have your progress block update another variable ina collection such as a dictionary or array and then you can access the progress value when required. – Paulw11 Feb 11 '17 at 22:24
  • I did try that, but then i need a timer that will update the ui so now I've got 2 processes running to update 1 progress view. It then it gets a bit tricky trying to manage a download queue with a timer that doesn't invalidate quickly enough for it to receive data about which collection view and cell to update the ui in. – Pippo Feb 11 '17 at 22:50
  • I would associate the progress status object with the cell and have a timer running in each cell that periodically refreshed its status based on its assigned status object or use NSNotification as rob suggested – Paulw11 Feb 12 '17 at 00:12

1 Answers1

10

Presuming that you're dismissing the old view controller with the collection view and presenting a new one, there are two problems here:

  • You're then trying to update cells in the collection view in the previous view controller; and

  • You're keeping a strong reference to the old view controller that was dismissed.

If this is the case, the goal is to decouple the progress updates from any particular view controller, collection view, or cell. You also probably want to decouple the item/row number, too, in case you insert/remove any cells at any time. The best way to handle this is notifications:

  1. Define a few constants used when defining the notifications:

    private let notificationName = Notification.Name(rawValue: "com.domain.app.downloadProgress")
    private let notificationIdentifierKey = "com.domain.app.download.identifier"
    private let notificationPercentKey = "com.domain.app.download.percent"
    
  2. Have your progressBlock post a notification rather than trying to update the UI directly:

    let percent: Float = ...
    let userInfo: [AnyHashable: Any] = [
        notificationIdentifierKey: identifier,
        notificationPercentKey: percent
    ]
    NotificationCenter.default.post(name: notificationName, object: nil, userInfo: userInfo)
    

    Please note that there are no reference to self here, which keeps the progress block from hanging on to your view controller.

  3. Define some function that you can use to identify which IndexPath corresponds to the identifier for your download. In my simple example, I'm just going to have an array of download identifiers and use that:

    var downloadIdentifiers = [String]()
    
    private func indexPath(for identifier: String) -> IndexPath? {
        if let item = downloadIdentifiers.index(of: identifier) {
            return IndexPath(item: item, section: 0)
        } else {
            return nil
        }
    }
    

    You'd probably have a download identifier as a property of some Download model object, and use that instead, but hopefully it illustrates the idea: Just have some way to identify the appropriate IndexPath for a given download. (By the way, this decoupling the IndexPath from what it was when you first created the download is important, in case you insert/remove any items from your collection view at any point.)

    Now, you may ask what should you use for the identifier. You might use the URL's absoluteString. You might use some other unique identifier. But I'd discourage you from relying solely on item/row numbers, because those can change (maybe not now, but perhaps later as you make the app more sophisticated, you might be inserting removing items).

  4. Have your collection view's view controller add itself as an observer of this notification, updating the appropriate progress view:

    private var observer: NSObjectProtocol!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        observer = NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: .main) { [weak self] notification in
            if let identifier = notification.userInfo?[notificationIdentifierKey] as? String,
                let percent = notification.userInfo?[notificationPercentKey] as? Float,
                let indexPath = self?.indexPath(for: identifier),
                let cell = self?.collectionView?.cellForItem(at: indexPath) as? InnerCollectionCell {
                    cell.progressView.setProgress(percent, animated: true)
            }
        }
    
        ...
    }
    
    deinit {
        NotificationCenter.default.removeObserver(observer)
    }
    

    Please note the [weak self] capture list, to make sure the notification observer doesn't cause a strong reference cycle with the view controller.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • This is a fantastic answer thank you. One problem i have with this which i didn't explain in the question is that the collectionView cells i wish to edit are nested inside another collectionView. So self doesn't hold reference to the innerCollectionView it holds reference to the outerCollectionView. Populating the collection is easy enough using the collectionView.tag then index path like: "innerCell.imageCell.image = self.multiPartArray[collectionView.tag][indexPath.item].image" but i now struggle to understand how to get reference to the collectionView that is collectionView.tag – Pippo Feb 12 '17 at 00:33
  • @Pippo - I still think you want to use `weak` reference. You don't want the download progress updates to keep strong references to any UIKit objects. Also, Apple has been discouraging the use of `tag` numbers for quite a while, too. In my opinion, it's better to use some unique identifier within your model. But you can do whatever you want. – Rob Feb 12 '17 at 00:41