Search code examples
swiftuicollectionviewipados

DidDeselect cells off screen


I am trying to show the selected cell in my UICollectionView by changing its border and shadow color. my issue is that my DidDeselect function causes a crash if I select a cell then scroll the selected cell of screen and select another cell, I understand that is because my DidDeselect is trying to deselect a cell that has been reused so my question is:

How do I remove the border from a cell that is no longer visible once it has scrolled off the screen

My DidSelect:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cell = collectionView.cellForItem(at: indexPath as IndexPath) as! PresentationCell
    cell.layer.borderColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)
    cell.layer.borderWidth = 3
    cell.layer.shadowColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)
}

My DidDeSelect:

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    let cellItem = collectionView.cellForItem(at: indexPath) as! PresentationCell
    cellItem.layer.borderColor = UIColor.clear.cgColor
    cellItem.layer.borderWidth = 3
    cellItem.layer.shadowColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
 }

It crashes on this line:

let cellItem = collectionView.cellForItem(at: indexPath) as! PresentationCell

Error is : Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

Which I understand is because the cell I selected first is no longer displayed because I've scrolled it out of view and then selected a cell at the bottom of the collectionView

How do I get around this?


Solution

  • It is a correct behavior. When you use the dequeueReusableCell cell are reused and memory gets allocated to visible cells only. So, if you try to pull a cell which is already deallocated then it will gives you nil.

    In your case, you need to have some custom implementation where you can hold the selection and apply the logic.

    Following is the working code...

    Declare the variable at top

    var mySelection = -1
    

    Use this code block for rest of logic

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
     {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
        cell.backgroundColor = .blue
        if mySelection == indexPath.row {
          applySelection(cell: cell, index: indexPath.row)
        } else {
          removeSelection(cell: cell, index: indexPath.row)
        }
        return cell
     }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) 
     {
        mySelection = indexPath.row
        if let cell = collectionView.cellForItem(at: indexPath as IndexPath) 
        {
          applySelection(cell: cell, index: indexPath.row)
        }
     }
    
    override func collectionView(_ collectionView: UICollectionView, 
     didDeselectItemAt indexPath: IndexPath) 
     {
        if let cellItem = collectionView.cellForItem(at: indexPath) 
        {
          removeSelection(cell: cellItem, index: indexPath.row)
        }
     }
    
    func applySelection(cell: UICollectionViewCell, index: Int) {
          cell.layer.borderColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)
          cell.layer.borderWidth = 3
          cell.layer.shadowColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)
     }
    
    func removeSelection(cell: UICollectionViewCell, index: Int) {
        cell.layer.borderColor = UIColor.clear.cgColor
        cell.layer.borderWidth = 3
        cell.layer.shadowColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
     }