如何缓存单元格并在每个单元格中嵌入了 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
}
}
}
在尝试实现您想要的东西时,我会考虑/考虑改变一些事情,这可能是我们看到这种奇怪的细胞行为的原因:
在处理集合视图时使用 indexPath.item
而不是 indexPath.row
在你 prepareForReuse
中,你实际上丢弃了 playerView
所以当你尝试从缓存中再次恢复播放器时 playerView?.player = player
- playerView
很可能为零,需要重新初始化
您不需要将单元格作为一个整体挂起,这可能有效,但我认为我们应该让集合视图执行回收单元格的任务。只挂在播放器上
我还没有这样做,但是,请考虑在丢弃单元格时如何处理删除通知观察者,因为您可能不止一次订阅通知,这可能会导致问题
我也没有这样做,但记得在视频播放完毕后从缓存中删除视频,因为您不希望单元格再做出反应
以下是我所做的一些小改动:
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)
}
}
经过这些改变,我想你会得到你想要的:
基本上我想做的是缓存单元格并让视频继续播放。当用户滚动回单元格时,视频应该只从播放的位置显示。
问题是玩家被移除并且单元格最终出现在随机单元格而不是其指定区域。
你需要有两个视频才能工作,我从这里下载了视频 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
}
}
}
在尝试实现您想要的东西时,我会考虑/考虑改变一些事情,这可能是我们看到这种奇怪的细胞行为的原因:
在处理集合视图时使用
indexPath.item
而不是indexPath.row
在你
prepareForReuse
中,你实际上丢弃了playerView
所以当你尝试从缓存中再次恢复播放器时playerView?.player = player
-playerView
很可能为零,需要重新初始化您不需要将单元格作为一个整体挂起,这可能有效,但我认为我们应该让集合视图执行回收单元格的任务。只挂在播放器上
我还没有这样做,但是,请考虑在丢弃单元格时如何处理删除通知观察者,因为您可能不止一次订阅通知,这可能会导致问题
我也没有这样做,但记得在视频播放完毕后从缓存中删除视频,因为您不希望单元格再做出反应
以下是我所做的一些小改动:
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)
}
}
经过这些改变,我想你会得到你想要的: