You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

这种写法的优势

  1. 完全遵循数据源驱动 UI 的设计,避免直接操作单元格带来的复用问题
  2. 状态统一管理,不会出现 UI 和数据不一致的情况
  3. 仅刷新需要更新的单元格,性能更优

内容的提问来源于stack exchange,提问作者fnisi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:21:16