How can I check if an indexPath is valid, thus avoiding an “attempt to scroll to invalid index path” error?

孤人 提交于 2019-12-03 22:04:27

You could check

- numberOfSections
- numberOfItemsInSection: 

of your UICollection​View​Data​Source to see if your indexPath is a valid one.

A more concise solution?

func indexPathIsValid(indexPath: NSIndexPath) -> Bool {
    if indexPath.section >= numberOfSectionsInCollectionView(collectionView) {
        return false
    }
    if indexPath.row >= collectionView.numberOfItemsInSection(indexPath.section) {
        return false
    }
    return true
}

or more compact, but less readable...

func indexPathIsValid(indexPath: NSIndexPath) -> Bool {
    return indexPath.section < numberOfSectionsInCollectionView(collectionView) && indexPath.row < collectionView.numberOfItemsInSection(indexPath.section)
}
Duncan C

@ABakerSmith's answer is close, but not quite right.

The answer depends on your model.

If you have a multi-section collection view (or table view for that matter - same issue) then it's pretty common to use an array of arrays to save your data.

The outer array contains your sections, and each inner array contains the rows for that section.

So you might have something like this:

struct TableViewData
{
  //Dummy structure, replaced with whatever you might use instead
  var heading: String
  var subHead: String
  var value: Int
}

typealias RowArray: [TableViewData]

typeAlias SectionArray: [RowArray]


var myTableViewData: SectionArray

In that case, when presented with an indexPath, you'd need to interrogate your model object (myTableViewData, in the above example)

The code might look like this:

func indexPathIsValid(theIndexPath: NSIndexPath) -> Bool
{
  let section = theIndexPath.section!
  let row = theIndexPath.row!
  if section > myTableViewData.count-1
  {
    return false
  }
  let aRow = myTableViewData[section]
  return aRow.count < row
}

EDIT:

@ABakerSmith has an interesting twist: Asking the data source. That way you can write a solution that works regardless of the data model. His code is close, but still not quite right. It should really be this:

func indexPathIsValid(indexPath: NSIndexPath) -> Bool 
{
  let section = indexPath.section!
  let row = indexPath.row!

  let lastSectionIndex = 
    numberOfSectionsInCollectionView(collectionView) - 1

  //Make sure the specified section exists
  if section > lastSectionIndex
  {
    return false
  }
  let rowCount = self.collectionView(
    collectionView, numberOfItemsInSection: indexPath.section) - 1

  return row <= rowCount
}

Here's a Swift 4 snippet I wrote and have been using for a while. It lets you either scroll to an IndexPath only if it's available, or - throw an error if the IndexPath is not available, to let you control what you want to do in this situation.

Check out the code here:

https://gist.github.com/freak4pc/0f244f41a5379f001571809197e72b90

It lets you do either:

myCollectionView.scrollToItemIfAvailable(at: indexPath, at: .top, animated: true)

Or

myCollectionView.scrollToItemOrThrow(at: indexPath, at: .top, animated: true)

The latter would throw something like:

expression unexpectedly raised an error: IndexPath [0, 2000] is not available. The last available IndexPath is [0, 36]

Using swift extension:

extension UICollectionView {

  func validate(indexPath: IndexPath) -> Bool {
    if indexPath.section >= numberOfSections {
      return false
    }

    if indexPath.row >= numberOfItems(inSection: indexPath.section) {
      return false
    }

    return true
  }

}

// Usage
let indexPath = IndexPath(item: 10, section: 0)

if sampleCollectionView.validate(indexPath: indexPath) {
  sampleCollectionView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition.centeredHorizontally, animated: true)
}

If you are trying to set the state of a cell in the collection view without knowing whether the index path is valid or not, you could try saving the indices for cells with a special state, and set the state of the cells while loading them.

Objective C version:

- (BOOL)indexPathIsValid:(NSIndexPath *)indexPath
{
    return indexPath.section < [self.collectionView numberOfSections] && indexPath.row < [self.collectionView numberOfItemsInSection:indexPath.section];
}

You should check the validation of the index paths that will be appended with the data source(future state) and the deletion of index paths with the current existing ones(present state).

extension UITableView {

func isValid(indexPath: IndexPath, inDataSource: Bool = false) -> Bool {
    guard
        let numberOfSections = inDataSource
            ? dataSource?.numberOfSections?(in: self)
            : numberOfSections,
        let numberOfRows = inDataSource
            ? dataSource?.tableView(self, numberOfRowsInSection: indexPath.section)
            : numberOfRows(inSection: indexPath.section)
    else {
        preconditionFailure("There must be a datasource to validate an index path")
    }
    return indexPath.section < numberOfSections && indexPath.row < numberOfRows
}

usage:

// insert    
tableView.insertRows(at: indexPaths.filter({ tableView.isValid(indexPath: $0, inDataSource: true) }), with: .top)

// remove
tableView.deleteRows(at: indexPaths.filter({ tableView.isValid(indexPath: $0) }), with: .top)

output:

true or false
Thorory

Perhaps this is what you're looking for?

- (UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath

Return Value: The cell object at the corresponding index path or nil if the cell is not visible or indexPath is out of range.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!