61
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-22

1/29: H_Craneさんに指摘して頂いた箇所を修正しました。
この記事は フラーAdventCalendar2019 の22日目の記事です。

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

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

RxSwift

用途別に紹介していきます。
まず、RxSwiftを最近触り始めたけど、よく理解できていない方にオススメです。

また、2019年のiOSDC自作して理解するリアクティブプログラミングフレームワークはとても参考になりました。(URLは動画となっています。)

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

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

MVVM

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

また、最近はiOSアプリ設計パターン入門 を購入して勉強しています。

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

概要

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

GithubRepository

 https://github.com/SUGIYOSI/StopWatchSample

画面の画像

sample1.png   sample2.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_1336.jpg

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

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

ViewModelInputOutputを分ける事で可読性をあげたり、バグを未然に防ぐ事ができます。
また、InputOutputは必ず DriverSignal を使用しています。

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

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

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は難しいなと思いました。

61
42
2

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
61
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?