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

如何避免CollectionView reloadData时当前可见单元格内代码执行?

解决CollectionView reloadData中断当前播放视频的问题

这是一个很常见的CollectionView分页+视频播放的坑,我来帮你分析问题根源并给出几个可行的解决方案:

问题背景

我在做一个全屏视频的CollectionView,同一时间只显示一个单元格,上下滚动时视频启停正常。但分页加载新数据调用reloadData()时出现问题:当用户滚动到倒数第二个条目触发分页,若此时该条目正在播放视频,reloadData()会让新增的每个条目都执行cellForItemAt,导致当前播放的视频被中断。我尝试在Video模型中添加isUserWatchingVideo属性拦截,但没生效,请问怎么阻止reloadData()时当前可见单元格内的播放代码执行?

相关代码

class Video { 
    var url: URL? 
    var videoId: String? 
    var postDate: Double? 
    var isUserWatchingVideo = false 
}

var dataSource = [Video]()

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
    cell.currentItem = indexPath.item
    cell.video = dataSource[indexPath.item]
    return cell
}

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // 停止视频播放
    guard let cell = cell as? MyCell else { return }
    cell.player.pause()
    // 重置状态
    dataSource[indexPath.item].isUserWatchingVideo = false
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    handlePagination()
}

func handlePagination() {
    // 模拟获取新视频数据
    let newVideos = [Video]() // 实际是请求到的新数据
    dataSource.append(contentsOf: newVideos)
    // 按发布日期排序
    dataSource.sort(by: { $0.postDate ?? 0 > $1.postDate ?? 0 })
    // 问题出在这里:reloadData会触发所有可见cell的cellForItem和didEndDisplaying
    collectionView.reloadData()
}

// 自定义Cell
class MyCell: UICollectionViewCell {
    var currentItem: Int?
    var player: AVPlayer = AVPlayer()
    var video: Video? {
        didSet {
            guard let video = video else { return }
            // 这里的判断没生效,因为reloadData时cell被重新赋值,isUserWatchingVideo可能被重置?
            if video.isUserWatchingVideo { return }
            video.isUserWatchingVideo = true
            // 初始化播放逻辑
            guard let url = video.url else { return }
            let asset = AVAsset(url: url)
            let playerItem = AVPlayerItem(asset: asset)
            player.replaceCurrentItem(with: playerItem)
            player.play()
        }
    }
}

核心问题分析

你遇到的问题本质是reloadData()会强制刷新所有可见单元格,导致当前正在播放的cell被重新复用/赋值,触发video的didSet逻辑,而之前的isUserWatchingVideo判断失效,主要原因是:

  1. 分页后排序改变了原播放条目的索引,currentItem与数据源的对应关系错乱
  2. reloadData()时didEndDisplaying会被调用,重置了原播放条目的isUserWatchingVideo为false,之后cellForItem重新赋值时又会触发播放

解决方案

方案1:避免全局reloadData,使用批量更新(推荐)

既然分页后需要排序,我们可以先计算新增数据插入的位置,然后用performBatchUpdates来更新CollectionView,这样只会刷新新增的单元格,不会影响当前可见的播放单元格。

修改handlePagination():

func handlePagination() {
    // 保存当前数据源的长度,用于后续判断新增数据
    let originalCount = dataSource.count
    // 获取新视频数据(替换为实际请求逻辑)
    let newVideos = fetchNewVideos()
    
    // 先添加到数据源再排序
    dataSource.append(contentsOf: newVideos)
    dataSource.sort(by: { $0.postDate ?? 0 > $1.postDate ?? 0 })
    
    // 计算新增数据在排序后的indexPaths
    let newIndexPaths = dataSource.enumerated()
        .filter { originalCount..<dataSource.count ~= $0.offset }
        .map { IndexPath(item: $0.offset, section: 0) }
    
    // 使用批量更新插入新数据,不影响已有可见单元格
    collectionView.performBatchUpdates({
        collectionView.insertItems(at: newIndexPaths)
    }, completion: nil)
}

// 模拟获取新数据的方法
private func fetchNewVideos() -> [Video] {
    // 实际这里是网络请求逻辑
    return [Video]()
}

注意:如果你的Video类有唯一标识(比如videoId),也可以用它来精准判断新增数据,避免依赖索引范围:

extension Video: Equatable {
    static func == (lhs: Video, rhs: Video) -> Bool {
        return lhs.videoId == rhs.videoId
    }
}

// 替换上面的newIndexPaths计算逻辑
let newIndexPaths = dataSource.enumerated()
    .filter { newVideos.contains($0.element) }
    .map { IndexPath(item: $0.offset, section: 0) }

这个方案从根源上避免了全局刷新,是最优解,完全不会干扰当前正在播放的视频。

方案2:维护当前播放索引,精准控制播放逻辑

如果必须使用reloadData(),可以在ViewController中维护当前正在播放的视频索引,在cell的赋值逻辑中精准判断,避免重复初始化播放。

步骤1:在ViewController中添加播放索引记录

var currentPlayingIndex: Int? // 记录当前正在播放的视频索引

步骤2:修改CollectionView代理方法

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
    cell.currentItem = indexPath.item
    cell.video = dataSource[indexPath.item]
    
    // 如果是当前播放的索引,直接恢复播放状态
    if indexPath.item == currentPlayingIndex {
        cell.player.play()
    }
    
    // 设置回调,让cell通知VC播放状态变化
    cell.onVideoStart = { [weak self] in
        self?.currentPlayingIndex = indexPath.item
    }
    
    return cell
}

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let cell = cell as? MyCell else { return }
    cell.player.pause()
    
    // 只有当该单元格不是当前播放的索引时,才重置状态
    if indexPath.item != currentPlayingIndex {
        dataSource[indexPath.item].isUserWatchingVideo = false
    }
}

步骤3:修改MyCell,添加播放回调

class MyCell: UICollectionViewCell {
    var currentItem: Int?
    var player: AVPlayer = AVPlayer()
    var onVideoStart: (() -> Void)? // 通知VC播放开始的回调
    
    var video: Video? {
        didSet {
            guard let video = video else { return }
            
            // 如果是当前播放的索引,跳过初始化逻辑,直接等待VC恢复播放
            if currentItem == (parentViewController as? YourViewController)?.currentPlayingIndex {
                return
            }
            
            // 重置状态并初始化播放
            video.isUserWatchingVideo = false
            guard let url = video.url else { return }
            
            let asset = AVAsset(url: url)
            let playerItem = AVPlayerItem(asset: asset)
            player.replaceCurrentItem(with: playerItem)
            player.play()
            
            video.isUserWatchingVideo = true
            onVideoStart?() // 通知VC更新播放索引
        }
    }
}

方案3:优化播放触发时机,结合CollectionView可见性

让cell只在真正成为可见单元格时才开始播放,利用willDisplay代理方法代替cell内部的didSet触发播放。

步骤1:修改ViewController的代理方法

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
    cell.currentItem = indexPath.item
    cell.video = dataSource[indexPath.item]
    // 这里只赋值数据,不触发播放
    return cell
}

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let cell = cell as? MyCell, let video = cell.video else { return }
    
    if video.isUserWatchingVideo {
        // 之前已经播放过,直接恢复播放
        cell.player.play()
    } else {
        // 首次播放,初始化并开始播放
        video.isUserWatchingVideo = true
        guard let url = video.url else { return }
        
        let asset = AVAsset(url: url)
        let playerItem = AVPlayerItem(asset: asset)
        cell.player.replaceCurrentItem(with: playerItem)
        cell.player.play()
    }
}

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let cell = cell as? MyCell else { return }
    cell.player.pause()
    dataSource[indexPath.item].isUserWatchingVideo = false
}

步骤2:简化MyCell的代码

class MyCell: UICollectionViewCell {
    var currentItem: Int?
    var player: AVPlayer = AVPlayer()
    var video: Video? // 仅作为数据存储,不处理播放逻辑
}

这样,reloadData()时,只有真正即将显示的单元格才会触发willDisplay,当前正在播放的单元格如果依然可见,会直接恢复播放,不会重新初始化,避免中断。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 15:32:45