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

### 概要
iOSアプリのウォークスルーでよく使用されている、RxSwiftとUIScrollViewを使ったページングの実装についてメモを書いていきます。
最近作った[ゴミアプリ](https://itunes.apple.com/jp/app/garbageapp/id1438834675?l=ja&ls=1&mt=8)という個人プロジェクトのアプリで実際に使用したコードを基にこの記事は書かれています。

### スクリーンショット
<img src="https://raw.github.com/wiki/higan96/RXSwiftScrollView/images/scrollview_sample.gif" width=50%>

### Code

```swift: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`に対し、`UIScrollView`の`contentOffset`に基づいた数値を加工して渡しているだけです。その加工と受け渡し部分でRxSwift（RxCocoa）を使っているのでコンパクトでわかりやすい、というだけです。

```swift
Observable.just(colors.count)
  .bind(to: pageControl.rx.numberOfPages)
  .disposed(by: bag)
```
`colors`が動的に変化する仕様ではないためRxSwiftを使った書き方するメリットは皆無なのですが、サンプルなので敢えて書いてみました。`colors`のカウント（ここでは`3`）を基にObservableを生成し、`pageControl`の`numberOfPages`にbindしています。

```swift
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.currentPage`に`bind`して、ページコントロールのドット位置が変わります。

```swift
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](https://github.com/higan96/RXSwiftScrollView)

また[ゴミアプリ](https://itunes.apple.com/jp/app/garbageapp/id1438834675?l=ja&ls=1&mt=8)という個人プロジェクトのアプリをリリースしました。
よかったらダウンロードしてみてください。