LoginSignup
6

More than 3 years have passed since last update.

UICollectionView+UIPageControlをRxで書いてみた

Last updated at Posted at 2019-07-29

はじめに

本業では、Rxをほぼ使わないので、Rx使いたい衝動から表題のようなものを作ってみた。
アプリ界隈の初回起動チュートリアルとかでよく使うあれですね。
最近のアプリのエンジニアなら誰しもが作ったことあるんじゃないかという、UIScrollView+UIPageControllですね。
調べるとめちゃくちゃでてきますが、今回記事に書くのは一番簡単そうに書ける方法かつ、カスタマイズ性もあるものかなと自分は思います。

どんなの?

今回はUIScrollViewの代わりにUICollectionViewを採用しています。
UICollectionViewだと横スクロールのコレクションもcell投入も見慣れた書き方ができるので、綺麗なコードに仕上がりますね。
他にはUIScrollViewの内部にUIStackView+UIView(複数)を採用しているのも見受けますね。
両者の差といえば、reuseが走るかどうかにあると思います。
アプリ内埋め込みで再利用が必要にないやつであれば、UIStackView+UIView(複数)を採用すると良いでしょう。

成果物

見栄え的に色変えるのも寂しいのでlottieのデモを入れてみました。

uicollectionview_uipagecontroll.gif

UIScrollView+UIPageControllのバインド

上記の記事が大変参考になりました。(丸パクリですがw)
これだけでスクロール位置に対してのページbindができます。
nextButtonの処理以外は綺麗だ!!

final class ViewController: UIViewController {

    @IBOutlet private weak var pageControll: UIPageControl!

    @IBOutlet private weak var collectionView: UICollectionView! {
        willSet {
            newValue.register(cellType: PageItemCell.self)
        }
    }

    @IBOutlet private weak var nextButton: UIButton!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        Observable.just(Const.lotties.count)                    // ページ数
            .bind(to: pageControll.rx.numberOfPages)            // ページコントロールにbind
            .disposed(by: disposeBag)

        collectionView.rx.didScroll                             // スクロール位置変更を流す
            .withLatestFrom(collectionView.rx.contentOffset)    // 最新値contentOffset
            .map { Int($0.x / UIScreen.main.bounds.width) }     // ページ番号に加工
            .bind(to: pageControll.rx.currentPage)              // ページコントロールの現在ページにバインド
            .disposed(by: disposeBag)

        nextButton.rx.tap
            .filter { [unowned self] in
                self.pageControll.currentPage < (Const.lotties.count - 1) // ページ数超えないようfilter
            }
            .subscribe(onNext: { [unowned self] _ in
                self.collectionView.setContentOffset(CGPoint(x: self.collectionView.contentOffset.x + UIScreen.main.bounds.width, y: 0), animated: true)  // スクロール移動
            })
            .disposed(by: disposeBag)
    }
}

extension ViewController {

    struct Const {
        static let lotties = [
            "test0",
            "test1",
            "test2"
        ]
    }
}

UICollectionViewDelegate

xib系のloadをtype-safeに扱えるReusableを採用しています。
かなり軽量ですので、自作してもいいところですが、今回はプライベートなので、ガンガンライブラリ扱います。

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return Const.lotties.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell: PageItemCell =  collectionView.dequeueReusableCell(for: indexPath)
        cell.setData(lottieFile: Const.lotties[indexPath.item])
        return cell
    }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return collectionView.frame.size
    }
}

まとめ

めちゃくちゃ短く、綺麗に書けましたね。
実際、delegateに頼った書き方でもこれよりそこまで長く書くことにはなりませんがw
私のRx書きたい欲が少し収まりました!

いろんなアプリで必要になるこのコンポーネント、是非参考にしながら実装していただけると幸いです。

下記がgithubのソースコードです。
https://github.com/doihei/UIScrollView-UIPageControll

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6