1/29: H_Craneさんに指摘して頂いた箇所を修正しました。
この記事は フラーAdventCalendar2019 の22日目の記事です。
今回は「RxSwift,MVVMを学習するのに参考になった記事を紹介する」 + 「RxSwift,MVVMを使って何か簡単なアプリを作成する」の2点について書いていきたいと思います。
そして、RxSwift初学者の人の参考になれば嬉しいです。
1.RxSwift,MVVMを学習するのに参考になった記事
RxSwift
用途別に紹介していきます。
まず、RxSwiftを最近触り始めたけど、よく理解できていない方にオススメです。
また、2019年のiOSDCの自作して理解するリアクティブプログラミングフレームワークはとても参考になりました。(URLは動画となっています。)
次はオペレーターを理解する上で参考にさせて頂いた記事です。
最後は、実践的な使用方法を学習する上で参考になった記事です。
- RxSwiftでの実装練習の記録ノート(前編:Observerパターンの例とUITableViewの例)
- RxSwiftでの実装練習の記録ノート(後編:DriverパターンとAPIへの通信を伴うMVVM構成のサンプル例)
- RxSwiftのすぐに取り込める使用例をまとめてみた
MVVM
自分はアーキテクチャって言葉自体聞いた事ない状態からだったので以下の記事は分かりやすく、ありがたかったです。
また、最近はiOSアプリ設計パターン入門 を購入して勉強しています。
2.RxSwift + MVVMで簡単なStopWatchアプリを作成する
概要
下記の画面の画像にあるように、Startボタンを押すと0.1秒ごとにカウントされ、Stopボタンが出ます。そして、Stopボタンを押すとカウントが止まってStartボタンとResetボタンが出てきます。
このように普通のStopWatchアプリです。
タイトルの方にもMVVMって書いてあるんですが、今回のアプリではModel
は使う機会がありませんでした。なので、ViewController
とViewModel
のみの実装となります。
また、StoryBoardを使わずにCodeのみで作成しました。
GithubRepository
https://github.com/SUGIYOSI/StopWatchSample
画面の画像
使用ライブラリ
- 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
下記のコードはViewを作成しているコードを抜かしているのでコピペしていじってみたい方は、Githubの方からしてください。
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 の倒し方)
- friends.nico iOSのリリースと実装について
- RxSwiftへ苦手意識がある方向けの RxSwift + MVVM でiOSサンプルコード書きました
- RxCocoa 4 の Signal と Relay のまとめ
また、Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)
を使用する際にこの記事と同じように躓いてしまったので参考にさせてもらいました。
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は難しいなと思いました。