[iOS] 자동 재생 무한스크롤 영상 배너 만들기 (2/2)

또상·2022년 3월 21일
0

iOS

목록 보기
20/47

4. 컬렉션뷰 타이머 설정

만약 모든 카드에 대해 5초만큼만 보여준다면,

타이머를 5초로 설정하고, 5초마다 카드를 움직이게 한다. (index를 하나씩 더해서) 그런데 현재 페이지가 마지막 페이지일 때 첫번째 페이지로 돌아가게 해야하기 때문에, 현재 페이지를 저장하는 nowPage 라는 변수를 하나 만들어서 저장하고,

아래처럼 함수를 짜주면 된다.

// 변수를 하나 추가하고,
private var nowPage: Int = 1

func setAutoCardTimer() {    
    let _: Timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { (Timer) in
        self.cardMove()
    }
}

func cardMove() {
    if nowPage == cardContents.count - 2 {
        collectionView.scrollToItem(at: [0, 1], at: .right, animated: false)
        nowPage = 1
        return
    }
    
    nowPage += 1
    collectionView.scrollToItem(at: [0, nowPage], at: .right, animated: true)
}

하지만... 나는 모든 카드에 대해 5초가 아니라, 사진에 대해 3초 영상에 대해 13.2초 (영상 길이만큼) 을 보여주고 싶었기 때문에... 페이지가 바뀔 때 마다, 타이머를 다르게 세팅해야 했다.

private var nowPage: Int = 1 {
    didSet {
        setAutoCardTimer()
    }
}

func setAutoCardTimer() {
    // timer 초기화 - 하지 않으면 이전의 타이머가 남아서 
    // 의도치 않은 타이밍에 카드가 넘어가는 현상이발생한다.
    timer?.invalidate()
    timer = nil
    
    // 영상의 종류가 하나라서 13.2 초로 통일했지만,
    // 영상도 여러 종류로 넣으려면 if - else 분기를 더 나누면 될 것 같다.
    // 이것보다 더 좋은 방법도 있을까??
    if cardContents[nowPage].hasSuffix("jpg") {
        timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { (Timer) in
            self.cardMove()
        }
    } else {
        timer = Timer.scheduledTimer(withTimeInterval: 13.2, repeats: false) { (Timer) in
            self.cardMove()
        }
    }
}

func cardMove() {
    if nowPage == cardContents.count - 2 {
        collectionView.scrollToItem(at: [0, 1], at: .right, animated: false)
        nowPage = 1
        return
    }
    
    nowPage += 1
    collectionView.scrollToItem(at: [0, nowPage], at: .right, animated: true)
}

그리고, 카드가 움직일 때, nowPage 변수도 새로 세팅해줘야 하기 때문에 scrollViewDidEndDecelerating 에서 설정해주었다.

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.frame.size.width != 0 {
            let value = (scrollView.contentOffset.x / scrollView.frame.width)
            pageControl.currentPage = Int(round(value))
        }
        playFirstVisibleVideo()
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let value = (scrollView.contentOffset.x / scrollView.frame.width)
        nowPage = Int(round(value))
        
        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()
                self?.nowPage = last
            })
        case cardContents.count - 1:
            collectionView.scrollToItem(at: [0, 1], at: .left, animated: false)
            nowPage = 1
        default:
            break
        }
    }
}

참고

스크롤뷰로 이미지 페이지처럼 넘기기 https://fomaios.tistory.com/entry/Swift-스크롤뷰로-이미지-페이지처럼-넘기기Image-Paging-with-UIScrollView

셀에 동영상이 들어있는 컬렉션뷰 스크롤에 맞춰 영상 재생되게 하기 (마치 트위터처럼)
https://mobiraft.com/ios/for-gods-sake-can-you-autoplay-video-in-list-ios/

자동 스크롤 배너 - 타이머 설정 https://gonslab.tistory.com/24



4. 영상 소리 겹치는 문제 고치기.

1. 백그라운드 갔다 왔을 때

위에까지만 하면 앱을 계속해서 실행하는 중에는 자동 재생이 될 때도, 스크롤을 해서 직접 움직일 때도 문제 없이 잘 동작하는데, 영상을 재생하다 백그라운드에 갔다 오면 영상이 멈춰있는 문제가 발생했다.

그래서, 백그라운드에 갔다 오는 이벤트를 잡아서 백그라운드로 갈 때, 타이머를 초기화하고, 영상을 멈춘 뒤, 다시 앞단으로 오면 타이머를 설정하고 영상을 재생하게 했다.

여기서 중요한 점은, 처음에 foregrond 를 willEnterForeground 이걸로 잡았었는데, 앱을 홈화면까지 내렸다가 다시 실행하는 경우는 잡혔지만, 백그라운드 앱 목록까지만 갔다가 다시 앱을 실행하면 잡히지 않았다는 점이다. 두 이벤트를 모두 잡으려면 didBEcomeActive 를 사용해야 한다.

// ViewController > viewDidLoad
// MARK: - Background Observer
        NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(appMovedBackground), name: UIScene.willDeactivateNotification, object: nil)


// ViewController
    // 지금은 화면이 하나 뿐이라 will disappear 가 호출될 일은 없지만,
    // 만약 이 화면에서 새로운 화면을 present 하거나, pushVC 하게 되면,
    // 이 때도 초기화가 필요하다.
    override func viewWillDisappear(_ animated: Bool) {
        initCardView()
    }

    @objc func appMovedToForeground(_ notification: Notification) {
        setAutoCardTimer()
        playFirstVisibleVideo()
    }
    
    @objc func appMovedBackground(_ notification: Notification) {
        // code to execute
        initCardView()
    }
    
    func initCardView() {
//        collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: .left, animated: false)
//        nowPage = 1
        timer?.invalidate()
        timer = nil
        playFirstVisibleVideo(false)
    }

이런 식의 소리가 겹쳐 들린다거나 하는 오류가 많았는데...
백그라운드에 갔다 왔을 때도 당연히 viewWillAppear 가 실행될 거라고 생각했기 때문이었다. view life cycle 공부가 시급하다...


2. 처음 로드될 때

영상이 처음 로드될 때, 0번에 들어간 무한 스크롤용 영상이 가끔 재생되는 경우가 있었다. playerView 를 쓸데없이 부르는 것보다는 이미지를 넣는게 좋아보여서 영상의 처음 부분과 같은 이미지를 0번에 넣고, 이미지이지만 3초가 아닌 13.2초의 타이머가 설정되도록 처리를 해주었다.

private var cardContents: [String] = ["picka.png", "0.jpg", "picka.mov", "1.jpg", "picka.mov", "2.jpg", "picka.mov", "0.jpg"]

func setAutoCardTimer() {
    timer?.invalidate()
    timer = nil
    
    if nowPage == 0 || cardContents[nowPage].hasSuffix("mov") {
        timer = Timer.scheduledTimer(withTimeInterval: 13.2, repeats: false) { (Timer) in
            self.cardMove()
        }
    } else {
        timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { (Timer) in
            self.cardMove()
        }
    }
}



6. pageControl 개수 맞추기

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.frame.size.width != 0 {
        let value = (scrollView.contentOffset.x / scrollView.frame.width)
        pageControl.currentPage = Int(round(value)) - 1
    }
    playFirstVisibleVideo()
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    pageControl.numberOfPages = cardContents.count - 2
    return self.cardContents.count
}



7. 남은 문제..

// PlayerView
private var assetPlayer: AVPlayer? {
    didSet {
        DispatchQueue.main.async {
            if let layer = self.layer as? AVPlayerLayer {
                layer.player = self.assetPlayer
            }
        }
    }
}

이 앱에는 다른 화면으로 넘어가는 부분이 없어서 안나는 것 같은데, 엄청나게 간헐적으로 self.layer 의 self 에 접근할 때 EXC_BAD_EXCESS KERN_INVAID_ADDRESS 가 난다...

  • 일단 커뮤니티에 질문을 이용해서 playerView 의 deinit 이후에 didSet 이 실행될 수 있다고 해서, 아래처럼 바꾸었다.
// PlayerView
private var deinited: Bool = false

private var assetPlayer: AVPlayer? {
    didSet {
        if !deinited {
            DispatchQueue.main.async {
                if let layer = self.layer as? AVPlayerLayer {
                    layer.player = self.assetPlayer
                }
            }
        }
    }
}

deinit {
    deinited = true
    cleanUp() // clean up 안에 assetPlayer = nil 이 있음.
}

전체 코드 : https://github.com/ddosang/AutomaticPlayingVideoBanner

버그 제보와 더 좋은 방법은 댓글로 제보 부탁드립니다😶‍🌫️

profile
0년차 iOS 개발자입니다.

0개의 댓글