swift - 如何缓存单元格并在每个单元格中嵌入了 avplayer 的 Collection View 中重用单元格?

标签 swift xcode collectionview avkit

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

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

您需要有两个视频才能实现此功能,我从此处下载了这些视频 https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4 我刚刚用两个不同的名称保存了同一个视频两次。

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

这里是代码的链接: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. 处理 Collection View 时使用 indexPath.item 而不是 indexPath.row

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

  3. 您不需要将单元作为一个整体来保存,这可能会起作用,但是我认为我们应该让 Collection View 完成回收单元的业务。只保留玩家

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

  5. 我也没有这样做,但请记住在视频结束时从缓存中删除视频,因为您不希望单元格再使用react

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

自定义单元格

// 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)
    }
}

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

AVPlayer resume restore in UICollectionView UIScrollView iOS Swift

关于swift - 如何缓存单元格并在每个单元格中嵌入了 avplayer 的 Collection View 中重用单元格?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71459442/

相关文章:

objective-c - 在哪里可以从 UIController 中找到 instantiateViewControllerWithIdentifier 的标识符?

ios - UICollectionView : How do I implement auto-resizing of UICollectionViewCell in terms of its height?

ios - collectionView didSelectItemAtIndexPath 在迁移到 Xcode 10.2 后停止工作

ios - TableView 单元格内的 Collection View

swift - 使用嵌套函数

objective-c - 如何在 Swift 中调用 Objective C 类方法

ios - Swift 2 OAuth2 领英连接

ios - 解析指针值返回 nil 的查询

c++ - Xcode C++ 错误阻止进一步输出超过 srand()

ios - 在索引处添加 NSMutableArray 的元素与其他 NSMutableArrays 的相应元素