はじめに
2012年頃からObjective-C/Javaでスマートフォンアプリ開発を始め、2014年頃からCocos2d-xにハマり、5年間そればかりやってきたら、時代は、Swift/Kotlin、ReactiveやらObserverやら、MVVMやらクリーンアーキテクチャだそうで、完全に取り残されました。どこから始めればよいかよく分からないので、なんとなくRxSwiftから始めてみたいと思います。
RxSwiftとは
RxとはReactive Extensionsの略で非同期処理やイベント処理などを宣言的に記述することができるライブラリ。
FRP(Functional Reactive Programming)の一種、ReactiveX(C#、Javaなど)ファミリーのひとつでもある。
Rxを使いこなせればありとあらゆるものをストリームとして扱うことが可能らしい。
リアクティブとは
リアクティブプログラミングとは、通知されてくるデータを受け取るたびに関連したプログラムが反応し(リアクション)して、処理を行うようにするプログラミングの考え方です。
引用元 : 須田智之 (2017/2/16)『RxJavaリアクティブプログラミング』 翔泳社
ストリームとは
Rxでは、様々な処理をストリームとして扱います。ストリームというのは時間順に並んだ進行中のイベントを表します。以下図はマーブルダイアグラムというもので、横の流れがストリームで、mergeしたりできるよ、ってことでしょう。
引用元:[https://rxmarbles.com/](https://rxmarbles.com/)
RxSwiftで主にできること
・UI イベント受け取り
・Web API レスポンス受け取り
・データの変化の監視
環境
Xcode10.3
Swift5.0.1
RxSwift 4.3.1
RxCocoa 4.3.1
準備
1.プロジェクト作成
Xcode > Create a new Xcode project > HelloRxSwift
2.ディレクトリに移動
$ cd HelloRxSwift
3.Podfile作成/編集
$ pod init
$ vi Podfile
# Podfile
use_frameworks!
target 'HelloRxSwift' do
pod 'RxSwift'
pod 'RxCocoa'
end
4.ライブラリのインストール
$ pod install
5.定義
import UIKit
import RxSwift
import RxCocoa
ObserverとObservable
Observer (イベント処理)
Observableからデータストリームを受け取るクラス
Observable (イベント検知/発生元、ストリーム)
Observableは文字の意味の通りで、監視可能なものを表すクラスを表します。
Observableのイメージは川です。ObserverイベントとしてonNext、onError、onCompleteが流れてきます。
Observableが通知するイベントは以下のようなものがあります。
| ObserverType | 説明 |
|---|---|
| onNext | 通常のイベントを通知する。複数回送れる |
| onError | エラーの発生を通知する。発生後はイベントが一切発生しない |
| onCompleted | 完了を通知する。発生後はイベントが一切発生しない |
Subscribe(購読)とDispose(解除)
Subscribe
イベントを受け取る側は subscribe メソッドでイベントの購読を行います。
Observableを作成することで川ができ、Subscribeでそこに流れてくるイベントを拾うイメージです。
Dispose
Disposeは購読を解除(破棄)するためのクラスで、dispose()メソッドを呼ぶことで明示的に購読を破棄するものです。
DisposeBag
DisposeBagは、Disposeインスタンスを貯め、自身が解放(deinit)されたときに管理している購読を全て自動で解(unsubscribe)します。いい感じで購読を破棄して、メモリーリークを回避してくれます。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
private var viewModel: HogeViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = HogeViewModel()
viewModel.helloWorldObservable
.subscribe(onNext: { [weak self] value in
print("value = \(value)")
})
.disposed(by: disposeBag)
viewModel.updateItem()
}
}
class HogeViewModel {
let helloWorldSubject = PublishSubject<String>()
var helloWorldObservable: Observable<String> {
return helloWorldSubject.asObservable()
}
func updateItem() {
helloWorldSubject.onNext("Hello World!")
helloWorldSubject.onNext("Hello World!!")
helloWorldSubject.onNext("Hello World!!!")
helloWorldSubject.onCompleted()
}
}
SubjectとRelay
・Subject > 通信処理やDB処理など、エラーが発生したときにその内容によって処理を分岐させたいときに使う
・Relay > RelayはSubjectの薄いラッパー定義。UIに値をBindする時に使います。.nextイベントを流すにはacceptメソッドを使います(onNextのみ流れることが保証されている場合、Relayを使うのが適切、onErrorなどが発生すると止まってしまうため)
SubjectやRelayはprivateとして定義することで、クラス外からのイベント発生を防ぎ、デバッグ困難化、アプリ肥大化を抑制できる。
| クラス名 | 説明 | イベント | バッファ |
|---|---|---|---|
| PublishSubject | 一切キャッシュしないSubject | onNext, onError, onComplete | 持たない |
| BehaviorSubject | 直近の値を1つだけキャッシュするSubjectで、初期値を与えることができる。 | onNext, onError, onComplete | 持つ |
| PublishRelay | 初期値なし、valueプロパティなし | onNext | 持たない |
| BehaviorRelay | 初期値あり、valueプロパティあり、 | onNext | 持つ |
バッファ
BehaviorSubject/Relayは、subscribe時に1つ過去のイベントを受け取ることができる。最初にsubcribeするときは、宣言時に設定した初期値を受け取る。
例:
let hogeSubject = PublishSubject<String>()
let hogeRelay = PublishRelay<String>()
hogeSubject.onNext("ほげ")
hogeRelay.accept("ほげ")
bindとOparator
bind
指定したオブジェクトに対し、イベントストリームを接続できます。RxSwiftでは単方向バインディング。振る舞いは、subscribeして値をセットすることとほぼ同じ。
import RxSwift
import RxCocoa
//...
private let disposeBag = DisposeBag()
//...
let label = UILabel()
label.frame = CGRect(x: 50, y: 75, width: 200, height: 21)
view.addSubview(label)
let textField = UITextField()
textField.frame = CGRect(x: 50, y: 150, width: 200, height: 21)
textField.placeholder = "入力してください"
view.addSubview(textField)
textField.rx.text
.bind(to: label.rx.text)
.disposed(by: disposeBag)
Operator
Operatorは、Observableに対してイベントの値の変換、絞り込みなどの加工を施して、新たにObservableを生成する仕組みです。また、ふたつのObservableのイベントを合成・結合することなども可能です。
Observable+StandardSequenceOperators
| オペレータ | 説明 |
|---|---|
| filter | 指定条件に合致するものだけ通過 |
| takeWhile | 先頭から指定条件に合致している間だけイベントを通知 |
| takeWhileWithIndex | takeWhile のインデックス付きバージョン |
| take | 先頭から指定個数だけ通知して完了 |
| takeLast | 最後から指定した数を通知して完了 |
| skip | 先頭から指定した数だけイベントをスキップ |
| skipWhile | 先頭から指定条件に合致している間だけイベントをスキップ |
| skipWhileWithIndex | skipWhile のインデックス付きバージョン |
| map | 各要素の内容を指定した処理で別の値や型に変換します。 |
| mapWithIndex | map のインデックス付きバージョン |
| flatMap | イベントを Observable に変換した上で、 それを並列実行して発行するイベントをマージ( map + merge )。 |
| flatMapWithIndex | flatMap のインデックス付きバージョン |
| flatMapFirst | イベントを Observable に変換して順次実行するが、実行中のものが完了するまでに来た Observable は無視 |
| flatMapLatest | イベントを Observable に変換し、常に最新の Observable を利用( map + switchLatest )。 |
| elementAt | 指定インデックス番号のイベントを通知して完了 |
| single | 1つだけしかイベントを発行しないことを保証 |
Observable+Multiple
| オペレータ | 説明 |
|---|---|
| combineLatest | 複数の Observable の最新値同士を組み合わせる |
| zip | 複数の Observable の内容を順番に組み合わせる |
| switchLatest | 次の Observable が来たらそちらにスイッチする |
| concat | 複数の Observable を完了するまで待って順次 subscribe する |
| merge | 複数の Observable を subscribe して、全てのイベントをマージして通知 |
| catchError | エラーを補足して復旧 |
| catchErrorJustReturn | エラーを捕捉して復旧し、引数に与えた値を代わりに通知 |
| takeUntil | 指定した Observable が何かイベントを発行すると終了 |
| skipUntil | 指定した Observable が onNext を発行するまで、元の Observable が発行したイベントを無視 |
| amb | 複数の Observable のうち、最初にイベントを発行した Observable を採用 |
| withLatestFrom | 元の Observable に指定した Observable の最新値を合成 |
Observable+Time
| オペレータ | 説明 |
|---|---|
| debounce | 指定期間新たなイベントが発行されなくなってから最後に発行されたイベントを発行 |
| throttle | 指定期間の計測が開始された最初のイベントと、指定期間新たなイベントが発行されなくなってから最後に発行されたイベントを発行 |
| sample | 引数に渡した Observable の発行タイミングで元の Observable の最新値を通知 |
| interval | 指定期間ごとにイベントを発生 |
| timer | 指定時間後にイベントを発生させて完了 |
| take | 先頭から指定時間の間のイベントだけ通知して完了 |
| skip | 先頭から指定時間の間のイベントだけ無視してスキップ |
| ignoreElements | 全てのイベントを無視して完了やエラーのみ通知 |
| delaySubscription | subscribe するまでの時間を遅らせる |
| buffer | 最大指定時間または最大指定個数ごとにイベントを配列にまとめて通知 |
| window | 最大指定時間または最大指定個数ごとにイベントを Observable にまとめて通知 |
| timeout | 指定時間イベントが通知されなければエラー |
Observable+Creation
| オペレータ | 説明 |
|---|---|
| create | 任意の処理を行う Observable を生成 |
| empty | onCompleted を発行するだけの Observable を生成 |
| never | onError, onCompleted も含めて一切のイベントを発生させない Observable を生成 |
| just | 指定した要素を1回通知して完了する Observable を生成 |
| error | onError を発行するだけの Observable を生成 |
| of | 引数で渡した要素が順に通知される Observable を生成 |
| deferred | subscribe されるたびに Observable を新しく生成 |
| generate | for文っぽい初期値と停止条件、生成処理を指定して Observable を生成 |
| repeatElement | 指定された要素を永遠に通知し続ける Observable を生成 |
| using | Observable と、それと同じ生存期間を持つリソースとを紐付け |
| range | 指定範囲の値を順に流す Observable を生成 |
| toObservable | 配列などのシーケンス型を Observable に変換 |
サンプルアプリ
TextFieldに文字を入力すると、bindされたLabelに文字が出力されるサンプルです。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
private var viewModel: HogeViewModel!
override func viewDidLoad() {
super.viewDidLoad()
//ラベル
let label = UILabel()
label.frame = CGRect(x: 50, y: 75, width: 200, height: 21)
view.addSubview(label)
//テキストフィールド
let textField = UITextField()
textField.frame = CGRect(x: 50, y: 150, width: 200, height: 21)
textField.placeholder = "入力してください"
view.addSubview(textField)
//textFieldのテキスト文字数を数えてLabelのテキストへ反映
textField.rx.text.map{text -> String? in
guard let text = text else { return nil}
return "\(text)(\(text.count)文字)"
}
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}
動作イメージ
最後に
おまけ
Observable+Binding
| オペレータ | 説明 |
|---|---|
| multicast | 指定した Subject を使った ConnectableObservable を返す |
| publish | PublishSubject を使った ConnectableObservable を返す |
| replay | 指定数をキャッシュする ReplaySubject を使った ConenctableObservable を返す |
| replayAll | 全てのイベントをキャッシュする ReplaySubject を使った ConnectableObservable を返す |
| refCount | ConnectableObservable の connect を subscribe の数で参照カウント管理 |
| share | キャッシュしない Cold-Hot 変換 = publish + refCount |
| shareReplay | キャッシュ付きの Cold-Hot 変換 = replay + refCount |
| shareReplayLatestWhileConnected | shareReplay(1) の最適化バージョン |
Observable+Concurrency
| オペレータ | 説明 |
|---|---|
| observeOn | イベントを受け取るスケジューラを指定 |
| subscribeOn | subscribe を実行するスケジューラを指定 |
Observable+Debug
| オペレータ | 説明 |
|---|---|
| debug | 流れてくるイベントを標準出力に print |
Observable+Single
| オペレータ | 説明 |
|---|---|
| distinctUntilChanged | 変化がない間はイベントをスキップ |
| doOn | パイプラインの途中に処理を挟み、イベントはそのまま通過させる |
| startWith | 先頭に指定した値を発行するイベントを付け加える |
| retry | エラーが発生したら再試行 |
| retryWhen | retry のもっと細かい制御ができる汎用バージョン |
| scan | reduce の途中経過もイベント発行するバージョン |
Disposables
| クラス | 説明 |
|---|---|
| AnonymousDisposable | dispose が呼ばれたときに実行するアクションを指定する |
| BinaryDisposable | 2つの Disposable をまとめる |
| BooleanDisposable | dispose されたかどうかを保持するだけ |
| CompositeDisposable | 一緒に dispose される Disposable をグループ化 |
| NopDisposable | dispose されても何もしない |
| RefCountDisposable | 参照カウントで管理する |
| ScheduledDisposable | 指定したスケジューラで dispose 処理をする |
| SerialDisposable | 新しいものが設定されると古いものを dispose して置き換える |
| SingleAssignmentDisposable | 1度だけ内部の Disposable を設定できる |
| StableCompositeDisposable | BinaryDisposable を返すだけ |
| DisposeBag | 追加されたものを自身が解放されるときにまとめて dispose する |
Schedulers
| クラス | 説明 |
|---|---|
| ConcurrentDispatchQueueScheduler | dispath_queue_t や QOS で指定する並列実行 |
| ConcurrentMainScheduler | subscribeOn に最適化されたメインスレッド指定 |
| CurrentThreadScheduler | 現在のスレッドで処理をキューイングして順次実行 |
| HistoricalScheduler | NSDate, NSTimeInterval を使う VirtualTimeScheduler |
| ImmediateScheduler | 現在のスレッドで処理を即時実行 |
| MainScheduler | observeOn に最適化されたメインスレッド指定 |
| OperationQueueScheduler | NSOperationQueue を使った非同期実行 |
| SerialDispatchQueueScheduler | dispatch_queue_t や QOS で指定する順次実行 |
| VirtualTimeScheduler | 時間の経過をプログラムで制御できる |
参考
比較して学ぶRxSwift入門 (技術の泉シリーズ(NextPublishing))
