만약 모든 카드에 대해 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
위에까지만 하면 앱을 계속해서 실행하는 중에는 자동 재생이 될 때도, 스크롤을 해서 직접 움직일 때도 문제 없이 잘 동작하는데, 영상을 재생하다 백그라운드에 갔다 오면 영상이 멈춰있는 문제가 발생했다.
그래서, 백그라운드에 갔다 오는 이벤트를 잡아서 백그라운드로 갈 때, 타이머를 초기화하고, 영상을 멈춘 뒤, 다시 앞단으로 오면 타이머를 설정하고 영상을 재생하게 했다.
여기서 중요한 점은, 처음에 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 공부가 시급하다...
영상이 처음 로드될 때, 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()
}
}
}
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
}
// 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
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
버그 제보와 더 좋은 방법은 댓글로 제보 부탁드립니다😶🌫️