'UICollection View "Reload Data" After Scrolling Crash

I have made a collection view with cells arranged in rows in columns using a great tutorial. I have added a button to a toolbar in the main view controller that calls collectionView.reloadData() as I want a user to be able to edit values which will in turn update the datasource and then reload the collection view to show the updates.

Running this on a simulator it works, but if any scrolling takes place it causes this crash *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'. If no scrolling has taken place then calling collectionView.reloadData() works. I can't find where this empty array is which is causing the crash. I have tried printing all the arrays that are used in the code in the console but none appear to be empty. Have tried commenting out various lines of code to try and narrow down where the problem is, it seems to be something in the override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? block. I have also tried reseting the collection view frame co-ordinates to 0 before reload data is called but that also didn't work. Have been stuck going round in circles for a few days which no luck. Any suggestions as to where I am going wrong would be hugely appreciated! My code so far is below (please excuse the long winded explanation and code);

View Controller

import UIKit

class ViewController: UIViewController {
  
 // variable to contain cell indexpaths sent from collectionViewFlowLayout.
 var cellIndexPaths = [IndexPath] ()
  
  @IBOutlet weak var collectionView: UICollectionView!
  
  @IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout! {
    
    didSet {
      collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }
  }
  
  @IBAction func editButtonPressed(_ sender: UIButton) {
    
    collectionView.reloadData()
  }
  
  @IBAction func doneButtonPressed(_ sender: UIButton) {
  }
  
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    collectionView.delegate = self
    collectionView.dataSource = self
  }
}

extension ViewController:UICollectionViewDataSource {
  
  func numberOfSections(in collectionView: UICollectionView) -> Int {
    2
  }
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    5
  }
  
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! collectionViewCell
    
    cell.backgroundColor = .green
    cell.textLabel.text = "\(indexPath) - AAAABBBBCCCC"
  
    return cell
  }
}

extension ViewController:UICollectionViewDelegate, IndexPathDelegate {
  
  func getIndexPaths(indexPathArray: Array<IndexPath>) {
    
    cellIndexPaths = indexPathArray.uniqueValues
  }
  
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    
    var cellArray = [UICollectionViewCell] ()
    
    print(cellIndexPaths)
   
    for indexPathItem in cellIndexPaths {
    
      if let cell = collectionView.cellForItem(at: indexPathItem) {
        
        if indexPathItem.section == indexPath.section {
          
          cellArray.append(cell)
        }
      }
    
      for cells in cellArray {
        
        cells.backgroundColor = .red
        
        Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { (timer) in
          cells.backgroundColor = .green
      
        }
      }
    }
  }
}


extension ViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
      
      let cell = collectionView.cellForItem(at: indexPath)
      
      if let safeCell = cell {
        
        let cellSize = CGSize(width: safeCell.frame.width, height: safeCell.frame.height)
        
        return cellSize
      } else {
        
        return CGSize (width: 300, height: 100)
      }
   }
}


extension Array where Element: Hashable {

    var uniqueValues: [Element] {
        var allowed = Set(self)
        return compactMap { allowed.remove($0) }
    }
}

Flow Layout

import UIKit

protocol IndexPathDelegate {
  
  func getIndexPaths(indexPathArray: Array<IndexPath>)
  
}

class collectionViewFlowLayout: UICollectionViewFlowLayout {
  

  override var collectionViewContentSize: CGSize {
    
    return CGSize(width: 10000000, height: 100000)
  }
  
  override func prepare() {
    

    setupAttributes()
    
    indexItemDelegate()
 
  }
  
  // MARK: - ATTRIBUTES FOR ALL CELLS
  
  private var allCellAttributes: [[UICollectionViewLayoutAttributes]] = []

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

      var layoutAttributes = [UICollectionViewLayoutAttributes]()


      for rowAttrs in allCellAttributes {
          for itemAttrs in rowAttrs where rect.intersects(itemAttrs.frame) {
              layoutAttributes.append(itemAttrs)
          }
      }

      return layoutAttributes
  }
  
  // MARK: - SETUP ATTRIBUTES
  
  var cellIndexPaths = [IndexPath] ()
  
  private func setupAttributes() {
      
      allCellAttributes = []

      var xOffset: CGFloat = 0
      var yOffset: CGFloat = 0

      
      for row in 0..<rowsCount {
          
          var rowAttrs: [UICollectionViewLayoutAttributes] = []
          xOffset = 0
        
        
          for col in 0..<columnsCount(in: row) {
             
              let itemSize = size(forRow: row, column: col)
  
            
              let indexPath = IndexPath(row: row, column: col)
          
          
              let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
              
            
                 attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral

              rowAttrs.append(attributes)

              xOffset += itemSize.width
            
              cellIndexPaths.append(indexPath)
          
          }

          yOffset += rowAttrs.last?.frame.height ?? 0.0
          allCellAttributes.append(rowAttrs)
      }
  }
  
  
  // MARK: - CONVERT SECTIONS TO ROWS, ITEMS TO COLUMNS
  
  private var rowsCount: Int {
      return collectionView!.numberOfSections
  }

  private func columnsCount(in row: Int) -> Int {
      return collectionView!.numberOfItems(inSection: row)
  }
  
  // MARK: - GET CELL SIZE
  
  private func size(forRow row: Int, column: Int) -> CGSize {
    
      guard let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout,
        
          let size = delegate.collectionView?(collectionView!, layout: self, sizeForItemAt: IndexPath(row: row, column: column)) else {
          assertionFailure("Implement collectionView(_,layout:,sizeForItemAt: in UICollectionViewDelegateFlowLayout")
          return .zero
      }

      return size
  }
  
  private func indexItemDelegate () {
    
    let delegate = collectionView?.delegate as? IndexPathDelegate
    
    delegate?.getIndexPaths(indexPathArray: cellIndexPaths)
  
  }
}

// MARK: - INDEX PATH EXTENSION

//creates index path with rows and columns instead of sections and items

private extension IndexPath {
    init(row: Int, column: Int) {
        self = IndexPath(item: column, section: row)
    }
}

Collection Cell

import UIKit

class collectionViewCell: UICollectionViewCell {
    
  @IBOutlet weak var textLabel: UILabel!
  
  override func awakeFromNib() {
      super.awakeFromNib()
      
      contentView.translatesAutoresizingMaskIntoConstraints = false
      
      NSLayoutConstraint.activate([
          contentView.leftAnchor.constraint(equalTo: leftAnchor),
          contentView.rightAnchor.constraint(equalTo: rightAnchor),
          contentView.topAnchor.constraint(equalTo: topAnchor),
          contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
      ])
  }
  
}


Solution 1:[1]

I have managed to get around the issue by using the UICollectionView.reloadSections(sections: IndexSet) method. This doesn't cause any crashes. I loop through all sections and add each section to an IndexSet variable then use that in the reload sections method like this;

 var indexSet = IndexSet()

 let rowCount = collectionView.numberOfSections

 for row in 0..<rowCount {
      
      indexSet = [row]
       
      collectionView.reloadSections(indexSet)
   }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Lebowski1213