RxSwift + MVVM の学習をしよう(サンプルアプリ付き) 


1/29: H_Craneさんに指摘して頂いた箇所を修正しました。
この記事は [フラーAdventCalendar2019](https://adventar.org/calendars/4155) の22日目の記事です。

今回は「RxSwift,MVVMを学習するのに参考になった記事を紹介する」 + 「RxSwift,MVVMを使って何か簡単なアプリを作成する」の２点について書いていきたいと思います。
そして、RxSwift初学者の人の参考になれば嬉しいです。


# 1.RxSwift,MVVMを学習するのに参考になった記事

## RxSwift

用途別に紹介していきます。
まず、[RxSwift](https://github.com/ReactiveX/RxSwift)を最近触り始めたけど、よく理解できていない方にオススメです。

- [[初心者]RxSwiftの道標[入門]](https://qiita.com/nakailand/items/8b54dc9b4b39b0809c57)
- [RxSwiftについてようやく理解できてきたのでまとめることにした(1)](https://qiita.com/_ha1f/items/e16ddc6017c4ad807c3c)
- [オブザーバーパターンから始めるRxSwift入門](https://qiita.com/k5n/items/17f845a75cce6b737d1e)

また、2019年の[iOSDC](https://iosdc.jp/2019/)の[自作して理解するリアクティブプログラミングフレームワーク](https://www.youtube.com/watch?v=WoPcfR01Kt0)はとても参考になりました。(URLは動画となっています。)

次はオペレーターを理解する上で参考にさせて頂いた記事です。

- [RxSwiftの機能カタログ](https://qiita.com/k5n/items/e80ab6bff4bbb170122d)
- [今日こそ理解するHot/Cold](https://www.slideshare.net/yukitakahashi3139241/hot-cold)

最後は、実践的な使用方法を学習する上で参考になった記事です。

- [RxSwiftでの実装練習の記録ノート（前編：Observerパターンの例とUITableViewの例）](https://qiita.com/fumiyasac@github/items/90d1ebaa0cd8c4558d96)
- [RxSwiftでの実装練習の記録ノート（後編：DriverパターンとAPIへの通信を伴うMVVM構成のサンプル例）](https://qiita.com/fumiyasac@github/items/da762ea512484a8291a3)
- [RxSwiftのすぐに取り込める使用例をまとめてみた](https://qiita.com/yuzushioh/items/634a96c6fd69de4acdd0)

## MVVM

自分はアーキテクチャって言葉自体聞いた事ない状態からだったので以下の記事は分かりやすく、ありがたかったです。

- [Webアプリケーション開発者から見た、MVCとMVP、そしてMVVMの違い](https://qiita.com/shinkuFencer/items/f2651073fb71416b6cd7)

- [RxExample MVVM のその先へ（Fat ViewModel の倒し方）
](https://qiita.com/Kuniwak/items/015cddcf37e854713a2e)

また、最近は[iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture) を購入して勉強しています。

# 2.RxSwift + MVVMで簡単なStopWatchアプリを作成する

## 概要

下記の画面の画像にあるように、Startボタンを押すと0.1秒ごとにカウントされ、Stopボタンが出ます。そして、Stopボタンを押すとカウントが止まってStartボタンとResetボタンが出てきます。
このように普通のStopWatchアプリです。
タイトルの方にもMVVMって書いてあるんですが、今回のアプリでは`Model`は使う機会がありませんでした。なので、`ViewController`と`ViewModel`のみの実装となります。
また、**StoryBoardを使わずにCodeのみ**で作成しました。

##### GithubRepository
　https://github.com/SUGIYOSI/StopWatchSample

##### 画面の画像

<img width="200" alt="sample1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407128/53494299-b8a6-0c98-5d30-951712448fc7.png">　　　<img width="210" alt="sample2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407128/db5724fc-797b-cd1d-a347-f4cd47ebf171.png">

##### 使用ライブラリ
 - RxSwift
 - RxCocoa
 - SnapKit(今回はあまり重要ではない)

##### 環境 

- Xcode 11.2.1
- Swift 5.1.2

## ViewController

今回使ったUI部品は以下の3つです。(上の画像を参考にしてください)
- `timerLabel (UILabel)`
- `startStopButton (UIButton)`
- `resetButton (UIButton)`

RxSwiftの処理を追うのは初めのうちは結構大変なので、今回はViewとViewModelの動きを以下のように細かく書きました。正確じゃないところもありますが、参考程度にはなると思います。ViewModelとViewControllerを照らし合わせて見てください。あと字が汚くてすみませんw

<img width="606" alt="IMG_1336.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407128/6f17cfd8-0550-037b-3cb8-be28776720dd.jpeg">


下記のコードは**Viewを作成しているコードを抜かしている**のでコピペしていじってみたい方は、Githubの方からしてください。


```swift:StopWatchViewController.swift
class StopWatchViewController: UIViewController {
    private var viewModel: StopWatchViewModelType
    private let disposeBag = DisposeBag()

    init(viewModel: StopWatchViewModelType) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSubView()
        bind()
        viewModel.inputs.isPauseTimer.accept(false)
    }
}

extension StopWatchViewController {
    func bind() {
        startStopButton.rx.tap.asSignal()
            .withLatestFrom(viewModel.outputs.isTimerWorked)
            .emit(onNext: { [weak self] isTimerStop in
                self?.viewModel.inputs.isPauseTimer.accept(!isTimerStop)
            })
            .disposed(by: disposeBag)

        resetButton.rx.tap.asSignal()
            .emit(to: viewModel.inputs.isResetButtonTaped)
            .disposed(by: disposeBag)

        viewModel.outputs.isTimerWorked
            .drive(onNext: { [weak self] isWorked in
                if isWorked {
                    self?.startStopButton.backgroundColor = UIColor(red: 255/255, green: 110/255, blue: 134/255, alpha: 1)
                    self?.startStopButton.setTitle("Stop", for: UIControl.State.normal)
                } else {
                    self?.startStopButton.backgroundColor = UIColor(red: 173/255, green: 247/255, blue: 181/255, alpha: 1)
                    self?.startStopButton.setTitle("Start", for: UIControl.State.normal)
                }
            })
            .disposed(by: disposeBag)

        viewModel.outputs.timerText
            .drive(timerLabel.rx.text)
            .disposed(by: disposeBag)

        viewModel.outputs.isResetButtonHidden
            .drive(resetButton.rx.isHidden)
            .disposed(by: disposeBag)
    }
}
```

## ViewModel

`ViewModel`は`Input`と`Output`を分ける事で可読性をあげたり、バグを未然に防ぐ事ができます。
また、`Input`、`Output`は必ず `Driver`、`Signal` を使用しています。

この`ViewModel`を作成するにあたって、以下の3つの記事を参考にしました。

- [RxExample MVVM のその先へ（Fat ViewModel の倒し方）](https://qiita.com/Kuniwak/items/015cddcf37e854713a2e)
- [friends.nico iOSのリリースと実装について](https://qiita.com/econa77/items/501e3d2a91bda612a0c4)
- [RxSwiftへ苦手意識がある方向けの RxSwift + MVVM でiOSサンプルコード書きました](https://blog.yokurin.app/entry/2019/04/30/180022)
- [RxCocoa 4 の Signal と Relay のまとめ](https://tech.mercari.com/entry/2017/12/04/103247)


また、`Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)`を使用する際に[この記事](https://qiita.com/akihisasen/items/96fb48050d94393962a2)と同じように躓いてしまったので参考にさせてもらいました。

```swift:StopWatchViewModel.swift
protocol StopWatchViewModelInputs: AnyObject {
    var isPauseTimer: PublishRelay<Bool> { get }
    var isResetButtonTaped: PublishRelay<Void> { get }
 }

protocol StopWatchViewModelOutputs: AnyObject {
    var isTimerWorked: Driver<Bool> { get }
    var timerText: Driver<String> { get }
    var isResetButtonHidden: Driver<Bool> { get }
}

protocol StopWatchViewModelType: AnyObject {
    var inputs: StopWatchViewModelInputs { get }
    var outputs: StopWatchViewModelOutputs { get }
}

final class StopWatchViewModel: StopWatchViewModelType, StopWatchViewModelInputs, StopWatchViewModelOutputs {
    var inputs: StopWatchViewModelInputs { return self }
    var outputs: StopWatchViewModelOutputs { return self }

    // MARK: - Input
    let isPauseTimer = PublishRelay<Bool>()
    var isResetButtonTaped = PublishRelay<Void>()

    // MARK: - Output
    let isTimerWorked: Driver<Bool>
    let timerText: Driver<String>
    let isResetButtonHidden: Driver<Bool>

    private let disposeBag = DisposeBag()
    private let totalTimeDuration = BehaviorRelay<Int>(value: 0)

    init() {
        isTimerWorked = isPauseTimer.asDriver(onErrorDriveWith: .empty())

        timerText = totalTimeDuration
            .map { String("\(Double($0) / 10)") }
            .asDriver(onErrorDriveWith: .empty())
        
        isResetButtonHidden = Observable.merge(isTimerWorked.asObservable(), isResetButtonTaped.map { _ in true }.asObservable())
            .skip(1)
            .asDriver(onErrorDriveWith: .empty())

        isTimerWorked.asObservable()
            .flatMapLatest { [weak self] isWorked -> Observable<Int> in
                if isWorked {
                    return Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)
                        .withLatestFrom(Observable<Int>.just(self?.totalTimeDuration.value ?? 0)) { ($0 + $1) }
                } else {
                    return Observable<Int>.just(self?.totalTimeDuration.value ?? 0)
                }
             }
            .bind(to: totalTimeDuration)
            .disposed(by: disposeBag)

        isResetButtonTaped.map { _ in 0 }
            .bind(to: totalTimeDuration)
            .disposed(by: disposeBag)
    }
}

```

# 最後に

StopWatchアプリの方で改善した方がいい箇所があればコメントしていただければ嬉しいです。
これからも自分のペースで楽しく学習していければなと思います。
やっぱりRxSwiftは難しいなと思いました。