How can I set up a collection view of AVPlayers to only play a video in the current selected cell?

Viewed 472

I have a UICollectionView setup that has cells of video posts from my database. Right now, when the collection view is loaded, all of the videos in the different cells start playing. I want the videos to not play in any cells except the selected cell so that the video audios don't play over each other. How can I do this? Here is the code...

The view controller:

import UIKit
import Photos

struct VideoModel {
    let username: String
    let videoFileURL: String
}

class BetaClipsViewController: UIViewController, UICollectionViewDelegate {
    
    private var collectionView: UICollectionView?
    
    private var data = [VideoModel]()
    
    /// Notification observer
    private var observer: NSObjectProtocol?
    
    /// All post models
    private var allClips: [(clip: Clip, owner: String)] = []
    
    private var viewModels = [[ClipFeedCellType]]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = ""
        
        //        for _ in 0..<10 {
        //            let model = VideoModel(username: "@CJMJM",
        //                                   videoFileURL: "https://firebasestorage.googleapis.com:443/v0/b/globe-e8b7f.appspot.com/o/clipvideos%2F1637024382.mp4?alt=media&token=c12d0481-f834-4a17-8eee-30595bdf0e8b")
        //            data.append(model)
        //        }
        
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.itemSize = CGSize(width: view.frame.size.width,
                                 height: view.frame.size.height)
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView?.register(ClipsCollectionViewCell.self,
                                 forCellWithReuseIdentifier: ClipsCollectionViewCell.identifier)
        collectionView?.isPagingEnabled = true
        collectionView?.delegate = self
        collectionView?.dataSource = self
        view.addSubview(collectionView!)
        
        fetchClips()
        
        observer = NotificationCenter.default.addObserver(
            forName: .didPostNotification,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.viewModels.removeAll()
            self?.fetchClips()
        }
        self.collectionView?.reloadData()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView?.frame = view.bounds
    }
    
    private func fetchClips() {
        //        guard let username = UserDefaults.standard.string(forKey: "username") else {
        //            return
        //        }
        let userGroup = DispatchGroup()
        userGroup.enter()
        
        var allClips: [(clip: Clip, owner: String)] = []
        
        
        DatabaseManager.shared.clips() { result in
            DispatchQueue.main.async {
                defer {
                    userGroup.leave()
                }
                
                switch result {
                case .success(let clips):
                    allClips.append(contentsOf: clips.compactMap({
                        (clip: $0, owner: $0.owner)
                    }))
                    
                case .failure:
                    break
                }
            }
        }
        
        userGroup.notify(queue: .main) {
            let group = DispatchGroup()
            self.allClips = allClips
            allClips.forEach { model in
                group.enter()
                self.createViewModel(
                    model: model.clip,
                    username: model.owner,
                    completion: { success in
                        defer {
                            group.leave()
                        }
                        if !success {
                            print("failed to create VM")
                        }
                    }
                )
            }
            
            group.notify(queue: .main) {
                self.sortData()
                self.collectionView?.reloadData()
            }
        }
    }
    
    private func sortData() {
        allClips = allClips.shuffled()
        
        viewModels = viewModels.shuffled()
    }
}

extension BetaClipsViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return viewModels.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModels[section].count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cellType = viewModels[indexPath.section][indexPath.row]
        switch cellType {
        case .clip(let viewModel):
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ClipsCollectionViewCell.identifier,
                                                                for: indexPath)
                    as? ClipsCollectionViewCell else {
                        fatalError()
                    }
            cell.delegate = self
            cell.configure(with: viewModel)
            return cell
        }
    }
}

extension BetaClipsViewController: ClipsCollectionViewCellDelegate {
    func didTapProfile(with model: VideoModel) {
        print("profile tapped")
        let owner = model.username
        DatabaseManager.shared.findUser(username: owner) { [weak self] user in
            DispatchQueue.main.async {
                guard let user = user else {
                    return
                }
                let vc = ProfileViewController(user: user)
                self?.navigationController?.pushViewController(vc, animated: true)
            }
        }
    }
    
    func didTapShare(with model: VideoModel) {
        print("tapped share")
    }
    
    func didTapNewClip(with model: VideoModel) {
        let vc = RecordViewController()
        navigationController?.pushViewController(vc, animated: true)
    }
    
}

extension BetaClipsViewController {
    func createViewModel(
        model: Clip,
        username: String,
        completion: @escaping (Bool) -> Void
    ) {
        //        StorageManager.shared.profilePictureURL(for: username) { [weak self] profilePictureURL in
        //            guard let clipURL = URL(string: model.clipUrlString),
        //                  let profilePhotoUrl = profilePictureURL else {
        //                      return
        //                  }
        
        let clipData: [ClipFeedCellType] = [
            .clip(viewModel: VideoModel(username: username,
                                        videoFileURL: model.clipUrlString))
        ]
        self.viewModels.append(clipData)
        completion(true)
        //        }
    }
}

The cell:

import UIKit
import AVFoundation

protocol ClipsCollectionViewCellDelegate: AnyObject {
    func didTapProfile(with model: VideoModel)
    
    func didTapShare(with model: VideoModel)
    
    func didTapNewClip(with model: VideoModel)
}

class ClipsCollectionViewCell: UICollectionViewCell {
    
    static let identifier = "ClipsCollectionViewCell"
    
    var playerLooper: NSObject?
    
    // Labels
    
    private let usernameLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.textColor = UIColor.systemPink.withAlphaComponent(0.5)
        label.backgroundColor = UIColor.systemPink.withAlphaComponent(0.1)
        label.clipsToBounds = true
        label.layer.cornerRadius = 8
        return label
    }()
    
    // Buttons
    
    private let profileButton: UIButton = {
        let button = UIButton()
        button.setBackgroundImage(UIImage(systemName: "person.circle"), for: .normal)
        button.tintColor = .white
        button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
        button.clipsToBounds = true
        button.layer.cornerRadius = 32
        button.isUserInteractionEnabled = true
        return button
    }()
    
    private let shareButton: UIButton = {
        let button = UIButton()
        button.setBackgroundImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
        button.tintColor = .white
        button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
        button.clipsToBounds = true
        button.layer.cornerRadius = 4
        button.isUserInteractionEnabled = true
        return button
    }()
    
    private let newClipButton: UIButton = {
        let button = UIButton()
        button.setBackgroundImage(UIImage(systemName: "plus"), for: .normal)
        button.tintColor = .systemOrange
        button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.1)
        button.clipsToBounds = true
        button.layer.cornerRadius = 25
        button.isUserInteractionEnabled = true
        return button
    }()
    
    private let videoContainer = UIView()
    
    // Delegate
    weak var delegate: ClipsCollectionViewCellDelegate?
    
    // Subviews
    var player: AVPlayer?
    
    private var model: VideoModel?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .black
        contentView.clipsToBounds = true
        addSubviews()
    }
    
    private func addSubviews() {
        
        contentView.addSubview(videoContainer)
        
        contentView.addSubview(usernameLabel)
        
        contentView.addSubview(profileButton)
        contentView.addSubview(shareButton)
        contentView.addSubview(newClipButton)
        
        // Add actions
        profileButton.addTarget(self, action: #selector(didTapProfileButton), for: .touchUpInside)
        shareButton.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
        newClipButton.addTarget(self, action: #selector(didTapNewClipButton), for: .touchUpInside)
        
        videoContainer.clipsToBounds = true
        
        contentView.sendSubviewToBack(videoContainer)
    }
    
    @objc private func didTapProfileButton() {
        guard let model = model else {
            return
        }
        delegate?.didTapProfile(with: model)
    }
    
    @objc private func didTapShareButton() {
        guard let model = model else {
            return
        }
        delegate?.didTapShare(with: model)
    }
    
    @objc private func didTapNewClipButton() {
        guard let model = model else {
            return
        }
        delegate?.didTapNewClip(with: model)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        videoContainer.frame = contentView.bounds
        
        let size = contentView.frame.size.width/7
        let width = contentView.frame.size.width
        let height = contentView.frame.size.height
        
        // Labels
        usernameLabel.frame = CGRect(x: (width-(size*3))/2, y: height-880-(size/2), width: size*3, height: size)
        
        // Buttons
        profileButton.frame = CGRect(x: width-(size*7), y: height-850-size, width: size, height: size)
        shareButton.frame = CGRect(x: width-size, y: height-850-size, width: size, height: size)
        newClipButton.frame = CGRect(x: width-size-10, y: height-175-size, width: size/1.25, height: size/1.25)
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        usernameLabel.text = nil
        player?.pause()
        player?.seek(to: CMTime.zero)
    }
    
    public func configure(with model: VideoModel) {
        self.model = model
        configureVideo()
        
        // Labels
        usernameLabel.text = "@" + model.username
    }
    
    private func configureVideo() {
        guard let model = model else {
            return
        }
        
        guard let url = URL(string: model.videoFileURL) else { return }
        player = AVPlayer(url: url)
        
        let playerView = AVPlayerLayer()
        playerView.player = player
        playerView.frame = contentView.bounds
        playerView.videoGravity = .resizeAspectFill
        videoContainer.layer.addSublayer(playerView)
        player?.volume = 5
        player?.play()
        player?.actionAtItemEnd = .none
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(playerItemDidReachEnd(notification:)),
                                               name: .AVPlayerItemDidPlayToEndTime,
                                               object: player?.currentItem)
    }
    
    @objc func playerItemDidReachEnd(notification: Notification) {
         if let playerItem = notification.object as? AVPlayerItem {
             playerItem.seek(to: .zero, completionHandler: nil)
         }
     }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}
0 Answers
Related