Swift4+Xcode9中UICollectionView选中不同单元格崩溃问题
问题分析与解决方案
首先,我们先来拆解你遇到的核心问题:选中新单元格时,collectionView.cellForItem(at: indexPath) as! MediaItemCollectionViewCell 强制转换崩溃,但选中已选中的单元格时却正常。最可能的原因和对应的解决思路如下:
为什么会崩溃?
1. cellForItem(at:) 返回 nil 导致强制转换失败
当你选中新单元格时,UICollectionView 的事件流程是:
- 先调用
didDeselectItemAt取消之前选中的单元格 - 再调用
didSelectItemAt选中新单元格
在这个过程中,新单元格可能因为复用机制、临时布局更新等原因,导致 cellForItem(at: indexPath) 返回 nil。而你用 as! 强制解包可选值,就会触发崩溃(错误信息看起来像是类型转换失败,但本质是 nil 解包异常)。
而选中已选中的单元格时,不会触发 didDeselectItemAt,单元格处于稳定的可见状态,cellForItem(at:) 能正常返回实例,因此不会崩溃。
2. 直接操作单元格的复用风险
你当前的写法是直接修改单元格的 UI,但 UICollectionView 的单元格是复用的。如果后续单元格被复用到其他位置,之前修改的状态会丢失,甚至引发类型混淆(虽然你只注册了一种单元格,但极端情况下快速操作可能导致复用异常)。
正确的解决方案:通过状态管理控制 UI
UICollectionView 的设计理念是数据源驱动 UI,你应该通过维护状态变量,在 cellForItemAt 中根据状态设置单元格内容,而不是直接在选中事件里修改单元格。
修改后的代码示例
// 维护当前播放的单元格索引 private var currentPlayingIndexPath: IndexPath? func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaItemCell", for: indexPath) as! MediaItemCollectionViewCell // 根据播放状态设置图片 if indexPath == currentPlayingIndexPath { cell.mediaItemCellImageView.image = UIImage(named: "stop.png") } else { cell.mediaItemCellImageView.image = mediaPlayer?.item(at: indexPath.row)?.image } return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let playingIndex = currentPlayingIndexPath { if playingIndex == indexPath { // 点击已播放的单元格:停止播放并恢复状态 mediaPlayer?.stopPlayingMedia() currentPlayingIndexPath = nil collectionView.reloadItems(at: [indexPath]) } else { // 切换播放:先停止之前的,再播放新的 mediaPlayer?.stopPlayingMedia() currentPlayingIndexPath = indexPath // 刷新之前和当前的单元格,更新UI collectionView.reloadItems(at: [playingIndex, indexPath]) mediaPlayer?.startPlayingMedia(at: indexPath.row) } } else { // 首次播放:更新状态并刷新单元格 currentPlayingIndexPath = indexPath collectionView.reloadItems(at: [indexPath]) mediaPlayer?.startPlayingMedia(at: indexPath.row) } } // 播放停止的代理方法 func mediaPlayerControllerDidFinishPlaying() { guard let index = currentPlayingIndexPath else { return } currentPlayingIndexPath = nil collectionView.reloadItems(at: [index]) timeLabel.text = "00:00" progressBar.progress = 0.0 }
这种写法的优势
- 完全遵循数据源驱动 UI 的设计,避免直接操作单元格带来的复用问题
- 状态统一管理,不会出现 UI 和数据不一致的情况
- 仅刷新需要更新的单元格,性能更优
内容的提问来源于stack exchange,提问作者fnisi
相关产品推荐
相关产品推荐

