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

Swift实现滚动暂停视频、单元格可见时播放视频的问题求助

问题描述

使用Swift开发,通过TableView实现垂直列表,每个TableViewCell嵌套CollectionView作为横向播放列表,目标实现类似YouTube的视频播放逻辑:CollectionView中的视频单元格完全可见时自动播放,不可见时暂停,采用AVPlayer处理播放。当前遇到问题:垂直滚动TableView到第2或第3个单元格时,最后一个视频会自动触发播放,需排查并修正。

数据结构示例

Table array:-   [Test:[video.mp4, image.jpg, image.jpg, image.jpg, image.jpg, image.jpg, video.mp4],Test2:[video.mp4, image.jpg, image.jpg, image.jpg, image.jpg, image.jpg, video.mp4],Test3:[video.mp4, image.jpg, image.jpg, image.jpg, image.jpg, image.jpg, video.mp4]]

Collection array:-   [video.mp4, image.jpg, image.jpg, image.jpg, image.jpg, image.jpg, video.mp4]

当前代码

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    stringArrayUrl.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoCell", for: indexPath) as! VideoCell
    if stringArrayUrl[indexPath.item].contains(".mp4") || stringArrayUrl[indexPath.item].contains(".MP4") {
        cell.videoMainView.isHidden = false
        cell.videoThumbImg.getThumbnailImageFromVideoUrl(url: URL(string: self.stringArrayUrl[indexPath.item])!) { (thumbNailImage) in
            cell.videoThumbImg.image = thumbNailImage
        }
        cell.imgPlayBtn.actionBlock {
            self.playVideoWhileScrlling(cell: cell, indexPath: indexPath)
        }
    }
    return cell
}

 func playVideoWhileScrlling(cell:StoryTableImageCell,indexPath:IndexPath){
    if self.stringArrayUrl.indices.contains(indexPath.item+1) {
        self.checkNextVideoUrlIsAvalibleOrNot(indexPath: indexPath.item)
    }
    cell.videoPlayerView.isHidden = false
    self.returnVideoPlayer(inputUrl: self.stringArrayUrl[indexPath.item])
}

func returnVideoPlayer(inputUrl:String) {
    let playerItem = AVPlayerItem(url: URL(string: inputUrl)!)
    videoPlayer.replaceCurrentItem(with: playerItem)
    videoPlayer.play()
}

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if let cell = cell as? VideoCell {
        if stringArrayUrl[indexPath.item].contains(".mp4") || stringArrayUrl[indexPath.item].contains(".MP4") {
            playVideoWhileScrlling(cell: cell, indexPath: indexPath)
        }
    }
}

代码问题分析
  1. 全局AVPlayer实例冲突:使用单一全局videoPlayer,不同CollectionViewCell触发播放时会替换同一个播放器的资源,导致滚动时播放状态混乱,甚至错误播放非目标视频。
  2. 可见性判断逻辑缺失:仅依赖willDisplay触发播放,无法判断单元格是否完全可见,也未处理单元格不可见时的暂停逻辑。
  3. 单元格复用未重置状态:CollectionViewCell复用后,未清理之前的播放状态,可能导致旧视频继续播放或错误触发播放。
  4. TableView滚动未联动处理:垂直滚动TableView时,未暂停已移出屏幕的TableViewCell中CollectionView的视频,同时新进入的单元格可能错误触发播放。

解决方案

1. 让每个VideoCell独立管理AVPlayer

修改VideoCell,使其自身持有播放器,避免全局实例冲突,同时处理复用状态重置:

class VideoCell: UICollectionViewCell {
    var videoPlayer: AVPlayer?
    var playerLayer: AVPlayerLayer?
    
    // 关联UI元素:videoThumbImg、imgPlayBtn、videoPlayerView、videoMainView
    @IBOutlet weak var videoThumbImg: UIImageView!
    @IBOutlet weak var imgPlayBtn: UIButton!
    @IBOutlet weak var videoPlayerView: UIView!
    @IBOutlet weak var videoMainView: UIView!
    
    override func prepareForReuse() {
        super.prepareForReuse()
        // 复用前清理播放状态
        pauseVideo()
        videoPlayer?.replaceCurrentItem(with: nil)
        videoPlayer = nil
        playerLayer?.removeFromSuperlayer()
        playerLayer = nil
        videoThumbImg.image = nil
        videoPlayerView.isHidden = true
        videoMainView.isHidden = true
    }
    
    // 播放视频
    func playVideo(with url: URL) {
        videoPlayer = AVPlayer(url: url)
        playerLayer = AVPlayerLayer(player: videoPlayer)
        playerLayer?.frame = videoPlayerView.bounds
        videoPlayerView.layer.addSublayer(playerLayer!)
        videoPlayer?.play()
        videoThumbImg.isHidden = true
        videoPlayerView.isHidden = false
    }
    
    // 暂停视频
    func pauseVideo() {
        videoPlayer?.pause()
        videoThumbImg.isHidden = false
        videoPlayerView.isHidden = true
    }
}

2. 准确判断CollectionViewCell的完全可见性

在嵌套CollectionView的TableViewCell中,监听滚动事件,仅播放完全可见的视频,同时暂停非可见视频:

class TableViewCellWithCollection: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
    @IBOutlet weak var collectionView: UICollectionView!
    var stringArrayUrl: [String] = []
    var lastPlayedIndexPath: IndexPath? // 记录最后播放的视频位置
    
    override func awakeFromNib() {
        super.awakeFromNib()
        collectionView.delegate = self
        collectionView.dataSource = self
        // 监听滚动偏移变化
        collectionView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "contentOffset" {
            updateVideoPlaybackState()
        }
    }
    
    deinit {
        collectionView.removeObserver(self, forKeyPath: "contentOffset")
    }
    
    // 更新视频播放状态
    func updateVideoPlaybackState() {
        let visibleIndexPaths = collectionView.indexPathsForVisibleItems
        let visibleCells = collectionView.visibleCells.compactMap { $0 as? VideoCell }
        
        // 暂停所有非完全可见的视频
        for cell in visibleCells {
            guard let indexPath = collectionView.indexPath(for: cell) else { continue }
            guard let cellFrame = collectionView.layoutAttributesForItem(at: indexPath)?.frame else { continue }
            
            // 判断单元格是否完全可见
            let isFullyVisible = collectionView.bounds.contains(cellFrame)
            if !isFullyVisible {
                cell.pauseVideo()
                if lastPlayedIndexPath == indexPath {
                    lastPlayedIndexPath = nil
                }
                continue
            }
            
            // 仅播放第一个完全可见的视频(避免多视频同时播放)
            let urlStr = stringArrayUrl[indexPath.item]
            if urlStr.lowercased().contains(".mp4") && lastPlayedIndexPath != indexPath {
                // 先暂停之前播放的视频
                if let lastIndex = lastPlayedIndexPath, let lastCell = collectionView.cellForItem(at: lastIndex) as? VideoCell {
                    lastCell.pauseVideo()
                }
                cell.playVideo(with: URL(string: urlStr)!)
                lastPlayedIndexPath = indexPath
            }
        }
    }
    
    // MARK: - CollectionView DataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        stringArrayUrl.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoCell", for: indexPath) as! VideoCell
        let urlStr = stringArrayUrl[indexPath.item]
        
        if urlStr.lowercased().contains(".mp4") {
            cell.videoMainView.isHidden = false
            // 获取视频缩略图
            cell.videoThumbImg.getThumbnailImageFromVideoUrl(url: URL(string: urlStr)!) { thumbNailImage in
                cell.videoThumbImg.image = thumbNailImage
            }
            // 点击播放按钮逻辑
            cell.imgPlayBtn.actionBlock { [weak self] in
                guard let self = self else { return }
                // 暂停其他视频
                if let lastIndex = self.lastPlayedIndexPath, let lastCell = self.collectionView.cellForItem(at: lastIndex) as? VideoCell {
                    lastCell.pauseVideo()
                }
                cell.playVideo(with: URL(string: urlStr)!)
                self.lastPlayedIndexPath = indexPath
            }
        } else {
            // 图片单元格,重置状态
            cell.videoMainView.isHidden = true
            cell.pauseVideo()
        }
        return cell
    }
    
    // MARK: - ScrollView Delegate
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updateVideoPlaybackState()
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            updateVideoPlaybackState()
        }
    }
}

3. TableView滚动时联动处理视频状态

在ViewController中,监听TableView滚动,暂停已移出屏幕的TableViewCell中的视频:

class YourViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var tableView: UITableView!
    var tableArray: [String: [String]] = [:]
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        tableArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withReuseIdentifier: "TableViewCellWithCollection", for: indexPath) as! TableViewCellWithCollection
        let key = Array(tableArray.keys)[indexPath.row]
        cell.stringArrayUrl = tableArray[key] ?? []
        return cell
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let visibleCells = tableView.visibleCells.compactMap { $0 as? TableViewCellWithCollection }
        
        // 遍历所有TableViewCell,暂停不在可见范围内的单元格中的视频
        for cell in tableView.visibleCells {
            guard let collectionCell = cell as? TableViewCellWithCollection else { continue }
            if !visibleCells.contains(collectionCell) {
                collectionCell.lastPlayedIndexPath = nil
                // 暂停该CollectionView中的所有视频
                for videoCell in collectionCell.collectionView.visibleCells.compactMap({ $0 as? VideoCell }) {
                    videoCell.pauseVideo()
                }
            }
        }
    }
}

4. 修复原代码类型错误

原代码中playVideoWhileScrlling方法的参数cell:StoryTableImageCell应为VideoCell,避免类型不匹配问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 19:45:53