🐥

無限かつ自動スクロールのcollectionView

2021/05/15に公開

概要

無限かつ自動スクロールのcollectionViewをつくりたい
バナー画像とかが延々と流れるあのやつです

実装

前提

  • UICollectionView
    • showsHorizontalScrollIndicator = false
    • isPagingEnabled = true
  • UICollectionViewFlowLayout
    • scrollDirection = .horizontal

それぞれのクラスのインスタンスのプロパティを上記に変更
次に実装を2つのパートに分けます。

①collectionViewを無限にスクロールできるようにする
②一定時間で自動でスクロールさせる

①collectionViewを無限にスクロールできるようにする

まず①から実装します。実装の方向性としては、最初の要素から一つ前にスクロールした場合は、スクロール完了後に最後の要素へ、最後の要素から一つ後ろへスクロールした場合は、スクロール完了後に最初の要素にとばす、という処理をします
ただ、普通にcollectionViewを実装すると最初の要素と最後の要素からはそれ以上前後にスクロールできないので少し工夫します
具体的には表示する配列を3倍にして、初期位置をずらすことによって実装します
イメージ的には、、

このような感じです。[0, 1, 2]が本来表示する配列です。この配列を3倍することによって、要素0Bから左にスクロールしている間はすぐ左の要素2Aが表示され、スクロール完了後には2Bにとばします。2Bから右にスクロールする場合も同様の考え方で実装します

//AutoScrollCollectionView = UICollectionViewのサブクラス
extension AutoScrollCollectionView: UICollectionViewDelegate { 
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        //スワイプ完了でコール
        if scrollView.contentOffset.x == scrollView.contentSize.width / 3 - scrollView.bounds.width {
            //2Aへのスクロール完了時
            self.contentOffset.x = (scrollView.contentSize.width / 3) * 2 - scrollView.bounds.width
        } else if scrollView.contentOffset.x == (scrollView.contentSize.width / 3) * 2 {
            //0Cへのスクロール完了時
            self.contentOffset.x = (scrollView.contentSize.width / 3)
        }
    }
}

②一定時間で自動でスクロールさせる

次にTimerを使って一定時間で自動でスクロールさせます。

class AutoScrollCollectionView: UICollectionView {
    var items: [Int] = [] {
        didSet {
            reloadData()
	    //初期位置を0Bに指定
            scrollToItem(at: .init(row: items.count, section: 0), at: .right, animated: false)
	    timer = Timer(timeInterval: 3.5, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
            if let timer = timer {
                RunLoop.current.add(timer, forMode: .common)
            }
        }
    }
    
    private var timer: Timer? {
        didSet {
            oldValue?.invalidate()
        }
    }
    
    //...イニシャライザなどは省略
    
    @objc private func autoScroll() {
        //現在表示されているセルのインデックスを取得
        let currentRow = Int(round(contentOffset.x / bounds.width))
        scrollToItem(at: .init(row: currentRow + 1, section: 0), at: .right, animated: true)
    }
}

ここで一つ注意が必要なことがあります。①で無限スクロールのための処理をscrollViewDidEndDecelerating内で実装していますがこのメソッドはscrollToItemでスクロールした場合には呼ばれません。scrollToItemでスクロールした場合にはscrollViewDidEndScrollingAnimationが呼ばれるので同じコードをこちらのメソッドでも実装します

extension AutoScrollCollectionView: UICollectionViewDelegate { 
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        //scrollToItemでコール
        if scrollView.contentOffset.x == scrollView.contentSize.width / 3 - scrollView.bounds.width {
            //2Aへのスクロール完了時
            self.contentOffset.x = (scrollView.contentSize.width / 3) * 2 - scrollView.bounds.width
        } else if scrollView.contentOffset.x == (scrollView.contentSize.width / 3) * 2 {
            //0Cへのスクロール完了時
            self.contentOffset.x = (scrollView.contentSize.width / 3)
        }
    }
}

補足

cellに対して配列の要素から情報を取得してレンダリングすることが多いかと思いますが、今回の実装では普通にindexPath[row]でアクセスすると、元の配列の3倍の個数のcellを返しているためout of rangeエラーになります。なので

items[indexPath.row % items.count]

でアクセスする必要があります

まとめ

もしかしたらライブラリでサクッと実装できそうですが、今回は勉強のためにも0から実装しました。メモリ管理とかを考えると遷移先のviewControllerなどで実装する場合はdidMoveToSuperviewなどでtimerをinvalidateする必要がありますがめんどくさいのではしょりました()

https://github.com/hara-94/autoScrollCollectionView

Discussion