Swift: удаление playerLayer в сбое prepareForReuse в CollectionViewCell

#ios #swift

#iOS #быстрый

Вопрос:

Я пытаюсь удалить AVPlayerLayer, чтобы избежать потенциальных проблем с памятью, но он выходит из строя.

У меня есть основной контроллер представления коллекции, который отображает TweetCells , и в каждом TweetCell из них у меня есть mediaCollectionView отображение горизонтальной прокрутки MediaCells . Каждая из этих медиасетей будет содержать AVPlayer, если отображаемый контент представляет собой видео.

Как я понял из нескольких сообщений SO, удаление AVPlayerLayer является правильной практикой, и поэтому я внедряю его в prepareForReuse .

Вот моя реализация:

 //At TweetCell
class TweetCell: UICollectionViewCell {

    lazy var mediaCollectionView: UICollectionView = {
        let size = NSCollectionLayoutSize(
            widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
            heightDimension: NSCollectionLayoutDimension.fractionalHeight(1)
        )
        
        let item = NSCollectionLayoutItem(layoutSize: size)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.backgroundColor = .systemGray
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.dataSource = self
        cv.delegate = self
        cv.register(MediaCell.self, forCellWithReuseIdentifier: "cell")
        return cv
    }()

    
    var tweet: Tweet? {
        didSet {
            if let tweet = tweet {
                //Setup other UI elements...
                
                //Refresh mediaCollectionView
                mediaCollectionView.reloadData()
                mediaCollectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .top, animated: false)
                
            }
        }
    } 

    override func prepareForReuse() {
        
        tweet = nil
        //Other UI elements reset, eg, profileImageView.image = nil, or tweetLabel.text = ""

        super.prepareForReuse()
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCell
        
        if tweet?.photosArray != nil {
            cell.media = tweet?.photosArray?[indexPath.item]

        } else if let videos = tweet?.videosArray {
            cell.media = videos[indexPath.item]
        } 
        return cell
    }

//At MediaCell
class MediaCell: UICollectionViewCell {
    
    let imageView: UIImageView = {
        let iv = UIImageView()
        iv.translatesAutoresizingMaskIntoConstraints = false
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
        iv.backgroundColor = .systemGreen
        return iv
    }()
    
    var videoView: AVPlayerView = {
        let v = AVPlayerView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.isHidden = false
        v.backgroundColor = .systemBlue
        return v
    }()
    
    var player : AVPlayer?
    var playerLayer: AVPlayerLayer? //Not in use
    
    var media: [String: AnyObject]? {
        didSet {
            guard let media = media else {return}

            if mediaType == "photo" {
                //PHOTOS
                
                ///Toggle image/video view
                removeAndDeactivateSubview(view: videoView)
                addAndActivateSubview(view: imageView)
        
                let url = URL(string: media["media_url_https"] as? String ?? "")
                imageView.sd_setImage(with: url)
                
            } else if mediaType == "video" {
                //VIDEOS
                
                ///Toggle image/video view
                removeAndDeactivateSubview(view: imageView)
                addAndActivateSubview(view: videoView)
                
                guard let urlString = media["url"] as? String else {return}
                guard let url = URL(string: urlString) else {return}
                
                player = AVPlayer(url: url)
                player?.isMuted = true
                let castedLayer = videoView.layer as? AVPlayerLayer
                castedLayer?.player = player
                
            } else {
                print("Some other media content type")
            }
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    func addAndActivateSubview(view: UIView) {
        addSubview(view)
        activateSubview(view: view)
    }
    
    func removeAndDeactivateSubview(view: UIView) {
        deactivateSubview(view: view)
        view.removeFromSuperview()
    }
    
    func activateSubview(view: UIView) {
        view.topAnchor.constraint(equalTo: topAnchor).isActive = true
        view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }
    
    func deactivateSubview(view: UIView) {
        view.topAnchor.constraint(equalTo: topAnchor).isActive = false
        view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = false
        view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = false
        view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = false
    }
    
    override func prepareForReuse() {
        imageView.image = nil
        player = nil
        
        //CRASH HERE
        if let layer = videoView.layer as? AVPlayerLayer {
            layer.player = nil
            layer.removeFromSuperlayer()
        }

        super.prepareForReuse()
    }

//Custome AVPlayerView
class AVPlayerView: UIView {
    override class var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
}
 

Сообщение о сбое указывает на .layer объект в if let layer = videoView.layer as? AVPlayerLayer со следующим сообщением:

 Thread 1: EXC_BAD_ACCESS (code=1, address=0x47cd649dd8e0)
 

После этого я попытался удалить AVPlayerLayer super.prepareForReuse , но он также выходит из строя.

Комментарии:

1. Вы пытались поместить этот код в tableViewDelegate->DidEndDisplatingCell вместо этого? Это может иметь значение

2. @ArikSegal Да, я пытался это сделать. Я поместил его в didEndDisplaying HomeController, и он разбился super.prepareForReuse() при TweetCell входе.