Edited at

RXSwiftを使ったUIScrollViewのページング


概要

iOSアプリのウォークスルーでよく使用されている、RxSwiftとUIScrollViewを使ったページングの実装についてメモを書いていきます。

最近作ったゴミアプリという個人プロジェクトのアプリで実際に使用したコードを基にこの記事は書かれています。


スクリーンショット


Code


ViewController

import UIKit

import RxSwift
import RxCocoa
import SnapKit

class ViewController: UIViewController {
@IBOutlet weak private var pageControl: UIPageControl!
@IBOutlet weak private var scrollView: UIScrollView!
private let bag = DisposeBag()

override func viewDidLoad() {
super.viewDidLoad()
let pageWidth = UIScreen.main.bounds.width
let pageHeight = scrollView.bounds.height
let colors: [UIColor] = [
.red, .green, .blue
]
scrollView.contentSize = CGSize(width: pageWidth * CGFloat(colors.count), height: pageHeight)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false

colors.enumerated().forEach { index, color in
let page = UIView()
let offset = CGFloat(index) * pageWidth
scrollView.addSubview(page)
page.backgroundColor = color
// SnapKitを使用しAutoLayoutの制約をコードで記述
page.snp.makeConstraints { make in
make.top.equalTo(scrollView)
make.left.equalTo(scrollView).offset(offset)
make.height.equalTo(scrollView)
make.width.equalTo(pageWidth)
}
}

Observable.just(colors.count)
.bind(to: pageControl.rx.numberOfPages)
.disposed(by: bag)

scrollView.rx.didScroll
.withLatestFrom(scrollView.rx.contentOffset)
.map { Int(round($0.x / pageWidth)) }
.bind(to: pageControl.rx.currentPage)
.disposed(by: bag)
}
}



解説

UIScrollViewのページング自体は特に実装側でやることはありません。気を使うのはcontentSizeとサブビューのサイズをあわせることぐらいでしょう。

基本的にやっていることはUIPageControlに対し、UIScrollViewcontentOffsetに基づいた数値を加工して渡しているだけです。その加工と受け渡し部分でRxSwift(RxCocoa)を使っているのでコンパクトでわかりやすい、というだけです。

Observable.just(colors.count)

.bind(to: pageControl.rx.numberOfPages)
.disposed(by: bag)

colorsが動的に変化する仕様ではないためRxSwiftを使った書き方するメリットは皆無なのですが、サンプルなので敢えて書いてみました。colorsのカウント(ここでは3)を基にObservableを生成し、pageControlnumberOfPagesにbindしています。

scrollView.rx.didScroll

.withLatestFrom(scrollView.rx.contentOffset)
.map { Int(round($0.x / pageWidth)) }
.bind(to: pageControl.rx.currentPage)
.disposed(by: bag)

流れとしては以下のようになります。

1. scrollView.rx.didScrollでスクロールのイベントを取得

2. withLatestFromで、そのタイミングでscrollView.rx.contentOffsetに変換しスクロール位置を取得

3. UIScrollViewの仕様ではスクロール位置がページの真ん中を超えた場合にページの切り替えだ行われます。その仕様に従い、contentOffset.xを1ページの横幅であるpageWidthでわり、四捨五入をページとしてmapで渡します。

4. pageControl.rx.currentPagebindして、ページコントロールのドット位置が変わります。

page.snp.makeConstraints { make in

make.top.equalTo(scrollView)
make.left.equalTo(scrollView).offset(offset)
make.height.equalTo(scrollView)
make.width.equalTo(pageWidth)
}

また細かい点ですが、このコードの場合Storyboardを使用したAutoLayoutでViewControllerのUIを記述しているため、サブビューとして生成するUIViewもCGRectなどは使わずAutoLayoutで記述します(ここではSnapKitというライブラリを使って記述しています)。

以上です。

こちらの記事のサンプルコードはGithubに上げました。higan96/RXSwiftScrollView

またゴミアプリという個人プロジェクトのアプリをリリースしました。

よかったらダウンロードしてみてください。