如何缓存单元格并在每个单元格中嵌入了 avplayer 的集合视图中重用单元格?

How do I cache cells and also reuse cells in a collectionview that has avplayers embedded in each cell?

基本上我想做的是缓存单元格并让视频继续播放。当用户滚动回单元格时,视频应该只从播放的位置显示。

问题是玩家被移除并且单元格最终出现在随机单元格而不是其指定区域。

你需要有两个视频才能工作,我从这里下载了视频 https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4 我只是用两个不同的名字将同一个视频保存了两次。

如何重现问题: 点击第一个单元格,然后一直向下滚动,然后向上滚动,您会注意到视频开始出现在各处。我只想让视频出现在正确的位置,而不是其他地方。

这里是 link 代码:Code link

class ViewController: UIViewController {
    
    private var collectionView: UICollectionView!
    
    private var videosURLs: [String] = [
        "ElephantsDream2", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream",
        "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream",
        "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream",
        "ElephantsDream", "ElephantsDream", "ElephantsDream", "ElephantsDream"
    ]
    
    var cacheItem = [String: (cell: CustomCell, player: AVPlayer)]()

    override func viewDidLoad() {
        super.viewDidLoad()
            
        setupCollectionView()
    }
    
    private func setupCollectionView() {
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: ColumnFlowLayout())
        view.addSubview(collectionView)

        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])
    }

}

extension ViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) as? CustomCell else { return }
        let item = videosURLs[indexPath.row]
        let viewModel = PlayerViewModel(fileName: item)
        cell.setupPlayerView(viewModel.player)
        cacheItem[item] = (cell, viewModel.player)
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        videosURLs.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let item = videosURLs[indexPath.row]
        if let cachedItem = cacheItem[item], indexPath.row == 0 {
            print(indexPath)
            print(item)
            cachedItem.cell.setUpFromCache(cachedItem.player)
            return cachedItem.cell
        } else {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? CustomCell else { return UICollectionViewCell() }
            cell.contentView.backgroundColor = .orange
            let url = Bundle.main.url(forResource: item, withExtension: "mp4")
            cell.playerItem = AVPlayerItem(url: url!)
            return cell
        }
    }
}


class CustomCell: UICollectionViewCell {
    private var cancelBag: Set<AnyCancellable> = []
    private(set) var playerView: PlayerView?
    var playerItem: AVPlayerItem?
    
    override var reuseIdentifier: String?{
        "cell"
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupViews()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        playerView = nil
        playerView?.removeFromSuperview()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupViews() {
        layer.cornerRadius = 8
        clipsToBounds = true
    }
    
    func setUpFromCache(_ player: AVPlayer) {
        playerView?.player = player
    }
    
    func setupPlayerView(_ player: AVPlayer) {
        if self.playerView == nil {
            self.playerView = PlayerView(player: player, gravity: .aspectFill)
            contentView.addSubview(playerView!)
            
            playerView?.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                playerView!.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                playerView!.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                playerView!.topAnchor.constraint(equalTo: contentView.topAnchor),
                playerView!.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
            
            playerView?.player?.play()
            
            NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime).sink { [weak self] notification in
                if let p = notification.object as? AVPlayerItem, p == player.currentItem {
                    self?.playerView?.removeFromSuperview()
                    guard let self = self else { return }
                    NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
                }
            }.store(in: &cancelBag)
        } else {
            playerView?.player?.pause()
            playerView?.removeFromSuperview()
            playerView = nil
        }
    }
}

在尝试实现您想要的东西时,我会考虑/考虑改变一些事情,这可能是我们看到这种奇怪的细胞行为的原因:

  1. 在处理集合视图时使用 indexPath.item 而不是 indexPath.row

  2. 在你 prepareForReuse 中,你实际上丢弃了 playerView 所以当你尝试从缓存中再次恢复播放器时 playerView?.player = player - playerView很可能为零,需要重新初始化

  3. 您不需要将单元格作为一个整体挂起,这可能有效,但我认为我们应该让集合视图执行回收单元格的任务。只挂在播放器上

  4. 我还没有这样做,但是,请考虑在丢弃单元格时如何处理删除通知观察者,因为您可能不止一次订阅通知,这可能会导致问题

  5. 我也没有这样做,但记得在视频播放完毕后从缓存中删除视频,因为您不希望单元格再做出反应

以下是我所做的一些小改动:

CustomCell

// I made this code into a function since I figured we might reuse it 
private func configurePlayerView(_ player: AVPlayer) {
    self.playerView = PlayerView(player: player, gravity: .aspectFill)
    contentView.addSubview(playerView!)
    
    playerView?.translatesAutoresizingMaskIntoConstraints = false
    
    NSLayoutConstraint.activate([
        playerView!.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
        playerView!.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
        playerView!.topAnchor.constraint(equalTo: contentView.topAnchor),
        playerView!.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
    ])
    
    playerView?.player?.play()
    
    NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime).sink { [weak self] notification in
        if let p = notification.object as? AVPlayerItem, p == player.currentItem {
            self?.playerView?.removeFromSuperview()
            guard let self = self else { return }
            NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
        }
    }.store(in: &cancelBag)
}

func setUpFromCache(_ player: AVPlayer) {
    // Check if the playerView needs to be set up
    if playerView == nil {
        configurePlayerView(player)
    }
    
    playerView?.player = player
}

func setupPlayerView(_ player: AVPlayer) {
    // Replace the code from here and use the function
    if self.playerView == nil {
        configurePlayerView(player)
    } else {
        playerView?.player?.pause()
        playerView?.removeFromSuperview()
        playerView = nil
    }
}

// No big change, this might have no impact, I changed the order
// of operations. You can use your previous code if it makes no difference
// but I added it for completeness
override func prepareForReuse() {
    playerView?.removeFromSuperview()
    playerView = nil
    
    super.prepareForReuse()
}

ViewController

// I use the index path as a whole as the key
// I only store a reference to the player, not the cell
var cacheItem = [IndexPath: AVPlayer]()

// Some changes have to be made to made to support the new
// cache type
func collectionView(_ collectionView: UICollectionView,
                    didSelectItemAt indexPath: IndexPath) {
    guard let cell
            = collectionView.cellForItem(at: indexPath) as? CustomCell else { return }
    let item = videosURLs[indexPath.item]
    let viewModel = PlayerViewModel(fileName: item)
    cell.setupPlayerView(viewModel.player)
    cacheItem[indexPath] = (viewModel.player)
}

// I set up a regular cell here
func collectionView(_ collectionView: UICollectionView,
                    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell
            = collectionView.dequeueReusableCell(withReuseIdentifier: "cell",
                                                 for: indexPath) as? CustomCell else { return UICollectionViewCell() }
    
    let item = videosURLs[indexPath.row]
    cell.contentView.backgroundColor = .orange
    let url = Bundle.main.url(forResource: item, withExtension: "mp4")
    cell.playerItem = AVPlayerItem(url: url!)
    return cell
}

// I manage restoring an already playing cell here
func collectionView(_ collectionView: UICollectionView,
                    willDisplay cell: UICollectionViewCell,
                    forItemAt indexPath: IndexPath)
{
    if let cachedItem = cacheItem[indexPath],
       let cell = cell as? CustomCell
    {
        print("playing")
        print(indexPath)
        print(cachedItem)
        cell.setUpFromCache(cachedItem)
    }
}

经过这些改变,我想你会得到你想要的: