[iOS]自動再生無限スクロールビデオバナーを作成(1/2)


本当に機能を実現するのに1週間かかったようですが...
たくさんのバグがあって本当に...疲れた.

1.横転コレクションビューを作成する



まず、ビデオを置かずに、画像だけを置いて、最後にビデオを置きます.
import UIKit
import SnapKit

class ViewController: UIViewController {
    
    typealias VideoCell = VideoCollectionViewCell
    typealias ImageCell = ImageCollectionViewCell
    
    // 카드에 들어갈 이미지를 넣은 배열.
    private var cardContents: [String] = ["0.jpg", "1.jpg", "2.jpg"]
    
    
    lazy var collectionView: UICollectionView = {
        
        // collection view layout setting
        let layout = UICollectionViewFlowLayout.init()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0
        layout.footerReferenceSize = .zero
        layout.headerReferenceSize = .zero
        
        // collection view setting
        let v = UICollectionView(frame: .zero, collectionViewLayout: layout)
        v.isScrollEnabled = true
        v.isPagingEnabled = true
        v.showsHorizontalScrollIndicator = false
        v.register(VideoCell.self, forCellWithReuseIdentifier: "VideoCell")
        v.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
        v.delegate = self
        v.dataSource = self
        
        // UI setting
        v.backgroundColor = UIColor.black
        v.layer.cornerRadius = 16
        
        return v
    }()
    
    lazy var pageControl = UIPageControl()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        self.view.backgroundColor = UIColor(red: 227/255, green: 219/255, blue: 235/255, alpha: 1)
        
        view.addSubview(collectionView)
        view.addSubview(pageControl)
        
        let edge = view.frame.width - 40
        
        collectionView.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.height.equalTo(edge)
        }
        
        pageControl.snp.makeConstraints { make in
            make.top.equalTo(collectionView.snp.bottom).offset(10)
            make.left.right.equalToSuperview()
        }
        
    }


}

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // page control 설정.
        if scrollView.frame.size.width != 0 {
            let value = (scrollView.contentOffset.x / scrollView.frame.width)
            pageControl.currentPage = Int(round(value))
        }
    }
}

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        pageControl.numberOfPages = cardContents.count
        return self.cardContents.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
        
        cell.configure(image: cardContents[indexPath.item])
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
      return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
    }
}
Image Cell
import UIKit

class ImageCollectionViewCell: UICollectionViewCell {
    
    private let imageView: UIImageView = {
        let v = UIImageView()
        v.contentMode = .scaleAspectFit
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupUI() {
        self.contentView.addSubview(imageView)
        
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
    
    func configure(image: String) {
        if let image = UIImage(named: image) {
            imageView.image = image
        }
    }
}
ここに実装すると、左と右がスクロール不可能な領域になります.

左右を無限にスクロールさせましょう!


現在の3つの配列の一番前に元の最後の画像が表示されます.
一番後ろに最初の画像を入れます.
private var cardContents: [String] = ["2.jpg", "0.jpg", "1.jpg", "2.jpg", "0.jpg"]
これで先ほどと同じように動いていましたが、写真だけで5つのviewに増えてしまいました…
ピチュからのビューが欲しかったので、最初のスタートは1番から、
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    collectionView.scrollToItem(at: [0, 1], at: .left, animated: false)
}
ピチュウから左に回ると、新しく追加されたインデックスは0番ライチュウ!!そしてシュッとしてからIndex 3号で移動すると!!左にスクロールすると無限スクロールが可能になります.(画像が同じなので、移動しているのは誰も見ていないので、ページコントロールは3つに変更せず、5つに保持されています.)

今index 3号の莱丘を右に歩いているときは?逆にすればいいんですよね?4番ピチュが出てきましたピューピュースライド1番ピコで移動すると、右に無限にスクロールできます.
// 스크롤 뷰의 감속이 끝났을 때 == 스크롤뷰가 멈출 때 == 다음 페이지로 넘어갔을 때!!
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let value = (scrollView.contentOffset.x / scrollView.frame.width)
        
        switch Int(round(value)) {
        case 0:
            let last = cardContents.count - 2
            self.collectionView.scrollToItem(at: [0, last], at: .left, animated: false)
        case cardContents.count - 1:
            self.collectionView.scrollToItem(at: [0, 1], at: .left, animated: false)
        default:
            break
        }
    }
リファレンス
https://medium.com/swlh/swift-make-infinite-scrolling-view-with-uicollectionview-cell-eedd2f9997a8

2.ビデオをセルに挿入


本来は集合ビューの代わりにスクロールビューで作成されていたのですが、最初はアプリを実行していたので、今は見えないカードの動画も自動で再生されるので、探してお勧めを探す!!見つけました.ベストリファレンス

タイミングはもう決まりました.次は自撮り棒でビデオを放送します!!入れます.ビデオを再生するために、AVPlayerを使用します.初めて利用したので、かなりうろうろしていましたが・・・

ビデオファイルをフォルダに入れて、資料を作って、ピチュビデオピカチュビデオ莱丘ビデオを出させます.
private var cardContents: [String] = ["picka.mov", "0.jpg", "picka.mov", "1.jpg", "picka.mov", "2.jpg", "picka.mov", "0.jpg"]
PlayerView/AVPlayayer+拡張/VideocollectionViewCellを作成します.
ここのファイルに従って貼り付けました.https://github.com/mobiraft/AutoPlayVideoInListExample

セクションの貼り付け


AVPlayer+Extension


コレクションビューが過去になるにつれて、元の再生中のビデオは停止するために変数isplayingを作成します.
// AVPlayer+Extension
import Foundation
import AVKit

extension AVPlayer {
    
    var isPlaying:Bool {
        get {
            return (self.rate != 0 && self.error == nil)
        }
    }
    
}

PlayerView


VideoIsMuted-マナーモード/音声モードでビデオ音声をオン/オフにする
AssetPlayer-ビデオを再生し、ビデオを戻して停止するために必要なPlayer
url-ビデオアドレス
残りは大体理解できるので、理解したいならゆっくり読んでください.(実は私にもわかりませんが…)
// PlayerView
import UIKit
import AVKit

class PlayerView: UIView {
    
    static var videoIsMuted: Bool = true

    override class var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
    
    private var assetPlayer:AVPlayer? {
        didSet {
            DispatchQueue.main.async {
                if let layer = self.layer as? AVPlayerLayer {
                    layer.player = self.assetPlayer
                }
            }
        }
    }
    
    private var playerItem:AVPlayerItem?
    private var urlAsset: AVURLAsset?
    
    var isMuted: Bool = true {
        didSet {
            self.assetPlayer?.isMuted = isMuted
        }
    }
    
    var url: URL?
    
    init() {
        super.init(frame: .zero)
        initialSetup()
    }
    
    required init?(coder: NSCoder) {
        super.init(frame: .zero)
        initialSetup()
    }
    
    private func initialSetup() {
        if let layer = self.layer as? AVPlayerLayer {
            layer.videoGravity = AVLayerVideoGravity.resizeAspect
        }
    }
    
    func prepareToPlay(withUrl url:URL, shouldPlayImmediately: Bool = false) {
        guard !(self.url == url && assetPlayer != nil && assetPlayer?.error == nil) else {
            if shouldPlayImmediately {
                play()
            }
            return
        }
        
        cleanUp()
        
        self.url = url
        
        let options = [AVURLAssetPreferPreciseDurationAndTimingKey : true]
        let urlAsset = AVURLAsset(url: url, options: options)
        self.urlAsset = urlAsset
        
        let keys = ["tracks"]
        urlAsset.loadValuesAsynchronously(forKeys: keys, completionHandler: { [weak self] in
            guard let strongSelf = self else { return }
            strongSelf.startLoading(urlAsset, shouldPlayImmediately)
        })
        NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
    }
    
    private func startLoading(_ asset: AVURLAsset, _ shouldPlayImmediately: Bool) {
        var error:NSError?
        let status:AVKeyValueStatus = asset.statusOfValue(forKey: "tracks", error: &error)
        if status == AVKeyValueStatus.loaded {
            let item = AVPlayerItem(asset: asset)
            self.playerItem = item
            self.assetPlayer = AVPlayer(playerItem: item)
            self.didFinishLoading(self.assetPlayer, shouldPlayImmediately)
        }
    }
    
    private func didFinishLoading(_ player: AVPlayer?, _ shouldPlayImmediately: Bool) {
        guard let player = player, shouldPlayImmediately else { return }
        DispatchQueue.main.async {
            player.play()
        }
    }
    
    @objc private func playerItemDidReachEnd(_ notification: Notification) {
        guard notification.object as? AVPlayerItem == self.playerItem else { return }
        DispatchQueue.main.async {
            guard let videoPlayer = self.assetPlayer else { return }
            videoPlayer.seek(to: .zero)
            // videoPlayer.play() // 내가 생각한 카드뷰는 한번 재생하고 끝나면서 다음 카드로 넘어가고 하는거라 play 를 또 하면 영상이 겹쳐 들리는 문제가 발생해서 뺐다.
        }
    }
    
    func play() {
        guard self.assetPlayer?.isPlaying == false else { return }
        DispatchQueue.main.async {
            self.assetPlayer?.play()
        }
    }
    
    func pause() {
        guard self.assetPlayer?.isPlaying == true else { return }
        DispatchQueue.main.async {
            self.assetPlayer?.pause()
            self.assetPlayer?.seek(to: .zero) // 여기도 셀을 떠났다가 해당 셀에 다시 들어가면 영상이 처음부터 실행되도록 하기 위해 변경.
        }
    }
    
    func cleanUp() {
        pause()
        urlAsset?.cancelLoading()
        urlAsset = nil
        assetPlayer = nil
        removeObservers()
    }
    
    func removeObservers() {
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
    }

    deinit {
        cleanUp()
    }
}

VideoCell


PlayerViewに参加し、PlayerViewを操作する方法を作成します.
import UIKit

class VideoCollectionViewCell: UICollectionViewCell {
    
    private let playerView = PlayerView()
    
    var url: URL?
    
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupUI() {
        self.contentView.addSubview(playerView)
        
        playerView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
    
    @objc
    func volumeAction(_ sender:UIButton) {
        sender.isSelected = !sender.isSelected
        playerView.isMuted = sender.isSelected
        PlayerView.videoIsMuted = sender.isSelected
    }
    
    func play() {
        if let url = url {
            playerView.prepareToPlay(withUrl: url, shouldPlayImmediately: true)
        }
    }
    
    func pause() {
        playerView.pause()
    }
    
    // 우리는 로컬 비디오를 재생할 것이므로, 이렇게!
    func configure(_ file: String) {
      let file = file.components(separatedBy: ".")
      
      guard let path = Bundle.main.path(forResource: file[0], ofType: file[1]) else {
        debugPrint( "\(file.joined(separator: ".")) not found")
        return
      }
      let url = URL(fileURLWithPath: path)
      self.url = url
      playerView.prepareToPlay(withUrl: url, shouldPlayImmediately: false)
    }
}

extension ViewController


コレクション・ビューでは、現在表示されているビデオの最初のもの(ページ化されたコードではなく、スクロール・コレクション・ビューで実装されたコード)です.カスタマイズしたいと思ったことはありますか?しかし、時間がかかる場合があります...1つの関数は、ビデオを再生するために使用され、もう1つは、停止した関数とビデオが画面内にあるかどうかを確認するために使用されます.
// ViewController
extension ViewController {
    
    func playFirstVisibleVideo(_ shouldPlay:Bool = true) {
        let cells = collectionView.visibleCells.sorted {
            collectionView.indexPath(for: $0)?.item ?? 0 < collectionView.indexPath(for: $1)?.item ?? 0
        }
        let videoCells = cells.compactMap({ $0 as? VideoCollectionViewCell })
        if videoCells.count > 0 {
            let firstVisibileCell = videoCells.first(where: { checkVideoFrameVisibility(ofCell: $0) })
            for videoCell in videoCells {
                if shouldPlay && firstVisibileCell == videoCell {
                    videoCell.play()
                }
                else {
                    videoCell.pause()
                }
            }
        }
    }
    
    func checkVideoFrameVisibility(ofCell cell: VideoCollectionViewCell) -> Bool {
        var cellRect = cell.containerView.bounds
        cellRect = cell.containerView.convert(cell.containerView.bounds, to: collectionView.superview)
        return collectionView.frame.contains(cellRect)
    }
    
}

ViewController


Video Cellを追加し、CellForItemAtを変更します.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
    if cardContents[indexPath.item].hasSuffix(".mov") {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoCell", for: indexPath) as! VideoCell
        cell.configure(video: cardContents[indexPath.item])
        return cell
    } else {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
        cell.configure(image: cardContents[indexPath.item])
        return cell
    }
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.frame.size.width != 0 {
        let value = (scrollView.contentOffset.x / scrollView.frame.width)
        pageControl.currentPage = Int(round(value))
    }
    playFirstVisibleVideo()
}
ここまで言うと、問題点は、うまく実施されているように見えますか...
1つ目から左にスクロールすると、0番のビデオが再生され、7番のビデオがスクロールされ、スクロールビデオDidScroll>playFirstVisibleVideoでは7番のビデオがキャプチャされず、再生や停止ができない現象が発生します.
だからscrollViewDidEndDeclaratingでscrollTooItemが発生してから遊んだことがありますが成功しませんでした...アニメーションを利用して終了時に実行することを実現しました.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    let value = (scrollView.contentOffset.x / scrollView.frame.width)
    
    switch Int(round(value)) {
    case 0:
        let last = cardContents.count - 2
        UIView.animate(withDuration: 0.01, animations: { [weak self] in
          self?.collectionView.scrollToItem(at: [0, last], at: .left, animated: false)
        }, completion: { [weak self] _ in
          self?.playFirstVisibleVideo()
        })
    case cardContents.count - 1:
        self.collectionView.scrollToItem(at: [0, 1], at: .left, animated: false)
    default:
        break
    }
}