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) } } }
代码问题分析
- 全局AVPlayer实例冲突:使用单一全局
videoPlayer,不同CollectionViewCell触发播放时会替换同一个播放器的资源,导致滚动时播放状态混乱,甚至错误播放非目标视频。 - 可见性判断逻辑缺失:仅依赖
willDisplay触发播放,无法判断单元格是否完全可见,也未处理单元格不可见时的暂停逻辑。 - 单元格复用未重置状态:CollectionViewCell复用后,未清理之前的播放状态,可能导致旧视频继续播放或错误触发播放。
- 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
相关产品推荐
相关产品推荐

