0

I have a view controller in my app that plays user videos and you can scroll to the right to go to the next video. I have a cell set up that loads the video from the URL in firebase storage and loads the other data. Here is the cell code:

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"
    
    // 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 = .resizeAspect
        videoContainer.layer.addSublayer(playerView)
        player?.volume = 0
        player?.play()
        player?.actionAtItemEnd = .none
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

Here is the view controller code:

import UIKit

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.users(for: username) { usernames in
            defer {
                userGroup.leave()
            }
            
            let users = usernames + [username]
            for current in users {
                userGroup.enter()
                DatabaseManager.shared.clips(for: current) { result in
                    DispatchQueue.main.async {
                        defer {
                            userGroup.leave()
                        }
                        
                        switch result {
                        case .success(let clips):
                            allClips.append(contentsOf: clips.compactMap({
                                (clip: $0, owner: current)
                            }))
                            
                        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.collectionView?.reloadData()
            }
        }
    }
}

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("profile 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)
        }
    }
}

I think everything is set up properly in the code so why do no cells show in the collectionView? The view controller shows up as completely blank besides the background color.

Globe
  • 514
  • 3
  • 17
  • Can you set a breakpoint on `self.collectionView?.reloadData()` and check if it got called? I think that some completion is never executed somehow, so dispatchGroup will not notify the next code block – iDevid Nov 17 '21 at 11:28
  • It turns out that the fetch posts function never even finished because there was an issue with the code I used to get data from the database. I fixed that and now everything works perfectly! I do have a new question posted about the `collectionView` so I would appreciate if you took a look at that. https://stackoverflow.com/questions/69998779/how-can-i-pause-avplayer-when-a-cell-is-not-selected-and-play-an-avplayer-when-a – Globe Nov 18 '21 at 03:18

0 Answers0