LoginSignup
820

More than 5 years have passed since last update.

オブザーバーパターンから始めるRxSwift入門

Last updated at Posted at 2016-07-06

はじめに

Rx とは C# 発祥の Reactive Extensions のことで、様々な言語に移植されています。RxSwift は Rx の Swift 版です。

様々な言語に移植されているのは、それだけ有用だからです。しかし Rx は有用である一方で学習コストが高く、導入の敷居が高いとみなされがちです。

ネットで Rx について検索すると、Reactive とは・・・関数指向うんたら・・・と、そりゃオブジェクト指向プログラマにはとっつきにくそうに感じるわな、と思うものが多いです。

ここではオブジェクト指向設計で一般的に利用されるオブザーバーパターンを置き換えるところから、Rx の利用方法を解説してみます。

以下のようにシリーズになっていますが、この記事の内容だけでも十分役に立つと思います。

  1. オブザーバーパターンから始めるRxSwift入門
  2. RxSwift入門(2) 非同期処理してみる
  3. RxSwiftを深く理解する
  4. RxSwiftの機能カタログ
  5. RxCocoaが提供するDriverって何?
  6. おまけ: bindToとかaddDisposableToとか鬱陶しくない?演算子に置き換えちゃえ!

フルに Rx を利用した設計については以下の記事を書いています。

  1. Rxを使った設計をビジュアル化する
  2. オブジェクト指向プログラミングからリアクティブプログラミングへ、そして関数型プログラミングとの関係

記事公開時点の最新安定版は ver 2.6.0 です。ver 3.2.0 に合わせて内容を見直しました。ネットで検索した時に古い記事を読んでも理解できるように、ver 2.x の時どうだったかも一応記載しておきます。

Observable

名前の通り監視可能なものを表すクラスが Observable です。Observable が通知するものは3種類あります。

種類 説明
onNext 通常のイベントを通知する。複数回送れる。
onError エラーの発生を通知する。発生後はイベントが一切発生しない。
onCompleted 完了を通知する。発生後はイベントが一切発生しない。

図にするとこんな感じです。この図の書き方をマーブルダイアグラム(Marble Diagram)と言います。

MarbleDiagram.png

横線は時間軸で、左から右に時間が流れます。丸がonNextで通知されるイベントを表しています。縦棒はonCompletedで通知されるイベントの完了を表します。上では使っていませんが、エラーで終わる場合は✖︎(バツ)で書きます。

onErrorとonCompletedはどちらか片方しか発生しませんし、一度発生すると二度と発生しません。どちらも発生しないというのはアリです。

Observable を公開する側のコード例はこんな感じです。

class Hoge {
  var event: Observable<Int> {
    // 略
  }
  // ...

受信する側は subscribe メソッドでイベントの購読を行います。

let disposable = hoge.event.subscribe(
  onNext: { value in
    // 通常イベント発生時の処理
  },
  onError: { error in
    // エラー発生時の処理
  },
  onCompleted: {
    // 完了時の処理
  }
)

戻り値は Disposable オブジェクトで、dispose メソッドを呼び出すことで購読の解除を行うことができます。1

3つのうち処理が必要ないものは引数を省略できます。またこれらのうち1つだけの処理を記述したい場合は、それぞれ専用のメソッド subscribeNext, subscribeError, subscribeCompleted が用意されていましたが、ver 3.0で廃止されました。

// ver 2.x 系では onNextの処理だけでいい場合は以下のように書けた
let disposable = event.subscribeNext { value in
  // 通常イベント発生時の処理
}

PublishSubject

Observable でイベントが受け取れることを説明したので、次はイベントを発生させる方法を説明しましょう。

Rx には Subject と呼ばれるものがあります。これは Observable でありつつ、イベントを発生させるための onNext/onError/onCompleted メソッドを提供しています。一番基本的な Subject が PublishSubject です。

使い方を見れば一目瞭然ですので、コード例を示します。

class Hoge {
  private let eventSubject = PublishSubject<Int>()

  var event: Observable<Int> { return eventSubject }

  func doSomething() {
    // 略
    eventSubject.onNext(1)  // イベント発行
  }
}

この例では doSomething メソッドが呼び出されたときに、PublishSubject の onNext メソッドを使ってイベントを発生させています。

Subject 自体が Observable でもあるので、Observable として外部に公開しています。Subject そのものを公開してしまうと、外部から onNext/onError/onCompleted を呼び出せてしまうので、カプセル化を崩してしまいます。

これで Hoge クラスは内部で発生したイベントを外部に通知することができるようになりました。

ここまででもうオブザーバーパターンを置き換えることができますね。

  • 送信する側
    • PublishSubject を private プロパティに持つ
    • それを Observable として公開する
    • イベントを通知するときは PublishSubject の onNext を呼び出す
  • 受信する側
    • Observable の subscribe を呼び出してイベントを購読する
    • subscribe の戻り値の Disposable オブジェクトを取っておく
    • 購読が必要なくなったら取っておいた Disposable オブジェクトの dispose メソッドを呼び出す

これだけです。

BehaviorSubject

BehaviorSubject は最後の値を覚えていて、subscribeすると即座にそれを最初に通知する Subject です。生成するときに初期値を指定できます。

BehaviorSubject.png

例えば「ボタンを非表示にするかどうかを表すBool値」をイベントとして流す Observable を考えてみます。

class Presenter {
  private let buttonHiddenSubject = BehaviorSubject(value: false) 

  var buttonHidden: Observable<Bool> { return buttonHiddenSubject }

  func start() {
    buttonHiddenSubject.onNext(true)
    // ...
  }

  func stop() {
    buttonHiddenSubject.onNext(false)
    // ...
  }
// ...

これを受信する側は以下のように subscribe することで、イベントで流れてくる値をボタンの hidden プロパティに反映します。

let disposable = presenter.buttonHidden.subscribe(onNext: { [button] in
  button.hidden = $0
})

この場合、subscribe した時点で「現在値」を渡して欲しいわけです。BehaviorSubject を使うと、今の状態を反映した上で変化があったらそれを反映するという動作が実現できます。

BehaviorSubject の value メソッドで現在値を取得することができます。ただし既に onCompleted / onError が発生した可能性があり、その場合は例外が発生します。

class Presenter {
  // 略

  func doSomething() {
    do {
      try let buttonHidden = buttonHiddenSubject.value()
      if buttonHidden {
        // ...
      }
    } catch {
      // buttonHidden が既に完了またはエラーで終了している場合
    }
  }
//...

iOS には Objective-C の時代から KVO (Key-Value Observing) というプロパティの値変化を監視する仕組みがありますが、以下の欠点があります。

  • Objective-C 互換のクラスでしか使えない
  • 複数のプロパティを監視した場合に、1つのメソッドに集中して通知されるため、条件分岐が必要でコードが読みにくい
  • 条件分岐の際に文字列でどのプロパティか判定するため、タイポがあってもコンパイルエラーにも実行時エラーにもならない

BehaviorSubject を使うと上記の欠点なく KVO を置き換えることができます。が、次に説明するVariableを使う方が便利です。

Variable

Variable は Rx に一般的に存在するものではなく、RxSwift が提供している独自のものです。これは BehaviorSubject の薄いラッパーで、onError を発生させる必要がないなら、BehaviorSubject を Variable に置き換えられます。

class Presenter {
  private let buttonHiddenVar = Variable(false)

  var buttonHidden: Observable<Bool> { return buttonHiddenVar.asObservable() }

  func start() {
    buttonHiddenVar.value = true
    // ...
  }

  func stop() {
    buttonHiddenVar.value = false
    // ...
  }
//...

Variable は Observable でないため、asObservable メソッドで変換します(中に持っている BehaviorSubject を返すようです)。

イベントを発行するには value プロパティに値を設定します。現在値も value プロパティから読み出せます。BehaviorSubject と違ってonError / onCompleted を明示的に発生させることはできないため、現在値取得で例外が発生することはありません

特に意識することはないかもしれませんが、Variable オブジェクトは自身が解放されるときに onCompleted を発行します。

read only でないプロパティの場合、Variable のまま外に出してしまうというのも手です。

class Config {
  private let disposable: Disposable

  let optionButtonEnabled = Variable(false)

  init() {
    disposable = optionButtonEnabled.asObservable()
      .subscribe(onNext: { newValue in
        // 値が設定されたときの処理
      }
  }

  deinit {
    disposable.dispose()
  }
  //...

ただし後から getter をカスタマイズしたくなっても簡単にはできません。一方でシンプルであること以外にも、後述する bindTo メソッドの引数に指定できるというメリットがあります。

メリットとデメリットを考慮して、Observable として公開するか Variable として公開するか判断しましょう。

bindTo

Subject や Variable は Observable の bindTo メソッドの引数として渡せます。この bindTo を使うと subscribe を使うより少し簡単にイベントをプロパティに接続できます。

let disposable = presenter.buttonHidden.subscribe(onNext: { [button] in
  button.hidden = $0
})
// これはbindToを使ってこう書ける
let disposable = presenter.buttonHidden.bindTo(button.rx.isHidden)

上の例で rx.isHidden というのは RxSwift に同梱されている RxCocoa が提供している UIView の拡張プロパティです(ver 2.x では ドットではなくアンダーバーで button.rx_hidden でした)。Observable として値変化を監視することもできます。

自前のクラスでもプロパティを Variable として公開しておけば bindTo で接続することができます。

なお bindTo は RxCocoa が提供している拡張メソッドです。画面部品とのバインドを目的として提供されているのでしょう。

Disposable

subscribe したときに戻り値として返される Disposable オブジェクトは、dispose メソッドを呼ぶと購読解除できるというのは既に説明しました。

ここではもう少し詳しく説明します。

Disposableを残しておく必要がない場合

Observable を subscribe しても、onError/onCompleted が発生してイベントが終了すると購読解除状態になるので、onError/onCompleted が発生する前に購読解除する必要がないなら、Disposable オブジェクトを残しておく必要はありません

onError/onCompeleted が発生しない Observable の購読でも、シングルトンでアプリが起動している間ずっと購読し続ける場合は、購読解除する必要がないので Disposable オブジェクトを残しておく必要はないですね。

DisposeBag

dispose メソッドを呼んで購読解除するタイミングってどういうときが多いかというと、購読しているオブジェクト自身が解放されるときだったりします。

だったらまとめて配列かなんかのコンテナに格納しといて、デストラクタで一気に全部 dispose したらいいんじゃないでしょうか?それと同様なことをやってくれるのが DisposeBag です。

Disposable の addDisposableTo メソッドの引数に指定して使います。なお RxSwift 4 からは addDisposableTo は廃止されるそうで、移行のために ver 3.2 から disposed(by: ) が追加されています。こちらを利用しましょう。

class MyViewController {
  // 略

  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    super.viewDidLoad()

    presenter.indicatorHidden
      .bindTo(indicator.rx.isHidden)
      .disposed(by: disposeBag)
    presenter.startButtonEnabled
      .bindTo(startButton.rx.isEnabled)
      .disposed(by: disposeBag)
    presenter.alertEvent
      .subscribe(onNext: { [unowned self] in self.showAlertWithTitle($0) })
      .disposed(by: disposeBag)
    // ...
  }
//...

上の例では MyViewController が解放されるときに disposeBag も解放され、管理下にある全ての Disposable の dispose が呼び出されます。2

いちいち addDisposableTo とか disposed(by: ) とか全部に書かなきゃいけないのが面倒という方は、「bindToとかaddDisposableToとか鬱陶しくない?演算子に置き換えちゃえ! 」をご覧ください。DisposeBag にまとめて登録する拡張メソッドを用意する方法が記載されています。

イベントの加工

イベントの加工がまるで配列を操作するかのように簡単にできるというのが Rx の大きな特徴です。ここでは Observable に用意されている operator と呼ばれるイベントを加工するメソッドの利用方法を幾つか紹介します。

ここに紹介されているもの以外にも様々な加工メソッドが用意されています。どのようなものがあるかは RxMarbles が参考になります(イベントをドラッグして動かせます!)。JavaScript版のRxJSの説明ですが、RxSwiftでもほとんど同じです。

map

map はイベントの各要素を別の要素に変換します。配列の map と同じですね。

例えば次のようなターゲット名を提供するクラスがあるとします。

class Action {
  var targetName: Observable<String> {
      // 略
  }
//...

画面上ではターゲット名が設定されていなければ、開始ボタンを押せないとします。

class Presenter {
  var startButtonEnabled: Observable<Bool> {
    return action.targetName.map { !$0.isEmpty }
  }
//...

上記では String 型の「ターゲット名」の Observable を、 Bool 型の「開始ボタンが有効かどうか」の Observable に変換しています。

map.png

補足:
map は新しい Observable インスタンスを生成して返すため、 startButtonEnabled を監視するものが複数ある場合、全く同じ動作をする Observable インスタンスが複数生成されてしまいます。また全く同じ加工処理が複数回実行されることになります。これらを回避する方法については後述します。以下すべてのイベント加工・合成の例についても同様です。

operatorを利用しないでやってみる

operator の便利さを知るためにも、また operator を使ったやり方に慣れないとやりがちな例としても、operator を使わないでイベントの加工をやってみようと思います。

上の例で map で加工していた部分を、全く同じ結果になるように map を使わないでやってみます。

class Presenter {
  private let startButtonEnabledVar: Variable(false)
  private let disposeBag = DisposeBag()

  var startButtonEnabled: Observable<Bool> {
    return startButtonEnabledVar.asObservable()
  }

  init(action: Action) {
    action.targetName
      .subscribe(onNext: { startButtonEnabledVar.value = !$0.isEmpty })
      .disposed(by: disposeBag)
  }
//...

単純に startButtonEnabled に対応する startButtonEnableVar という Variable を作って、targetName の値を subscirbe して加工したものを伝搬させています。

map を使ったほうが随分すっきりしますよね?こんな風に何かのイベントを加工して伝搬するだけの Variable や Subject を用意したくなったら、operator を使って加工できないか考えてみてください。

filter

これも配列の filter と同様です。指定条件が true になるイベントだけを通過させます。

例えば「動作中」かどうかを表す running プロパティがあるとします。

class Engine {
  var running: Observable<Bool> {
    // 略
  }
//...

これを「動作開始したことを通知してくれるイベント」に変換することを考えます。

  var startEvent: Observable<Void> {
    return running.filter{ $0 }.map{ () }
  }

上記では filter で running が true に変化した時だけを抜き出し、true / false を伝達する必要はないので map で Void に変換しています。

filter.png

take

take はイベントを最初の指定した数だけに絞ります。指定数に達した時点で onCompleted になります。

先ほどの Engine.running を監視して、最初に起動開始したときだけ何かを実行したいとします。

_ = engine.running
  .filter{ $0 }
  .take(1)
  .subscribe(onNext: { _ in /* 処理 */ })

まず filter で true になったときだけに絞ります。そして take で最初の1つだけを監視します。onCompleted になるまで監視解除する必要がないなら、上記例のように戻り値は無視して構いません。

take.png

skip

skip はイベントの最初から指定個を無視します。

BehaviorSubject(またはそれを内部で利用している Variable)の現在値を無視して、PublishSubject のように変化だけを監視したいとします。

engine.running
  .skip(1)
  .subscribe(onNext: { running in
    // running変化時の処理
  })
  .disposed(by: disposeBag)

skip(1) すれば最初の現在値を無視して、変化だけを監視することができます。

skip.png

今までの Engine.runnning を使った例は購読時に running が false であることを前提としていました。そうでない場合は skip(1) を付ける必要があります。もちろん購読時点で running == true の場合にも通知してほしいなら必要ありません。 

イベントの合成

merge

merge を使うと、2つの同じ型のイベントストリームを1つにまとめることができます。

例えばそれぞれ別のクラスが発行するエラーイベントを、まとめて1つのエラーイベントにしたいとします。

  var errorEvent: Observable<ErrorType> {
    return Observable.of(hoge.errorEvent, fuga.errorEvent).merge()
  }

RxSwift での merge の呼び出し方は他の言語とはちょっと違っていて of を使います。3

merge.png

combineLatest

combineLatest は(違う型でも可の)直近の最新値同士を組み合わせたイベントを作ります。

例えば現在位置を通知する LocationMonitor クラスに現在位置の緯度経度を表す location プロパティがあるとします。LatLon は緯度経度を表すクラスです。

class LocationMonitor {
  var location: Observable<LatLon> {
    return locationVar.asObservable()
  }
//...

案内中の経路を表す Route クラスに目的地の緯度経度を表す goal プロパティがあるとします。

class Route {
  var goal: Observable<LatLon> {
    return goalVar.asObservable()
  }
//...

これらから目的地までの距離を表す Observable を作り出してみます。

  var distance: Observable<Double> {
    return Observable.combineLatest(locationMonitor.location, route.goal) {
      location, goal in goal.distanceFrom(location)
    }
  }

2つの Observable を combineLatest で合成しています。どちらかの Observable に変化があると、その変化値ともう一方の直近の値がクロージャに渡され、クロージャの戻り値がイベントとして流れます。

combineLatest.png

sample

sample はある Observable の値を発行するタイミングを、もう一方の Observable をトリガーして決める場合に使います。

例えば画面上にある部品が、押すと移動できるようになっているとします。移動中は position プロパティが位置を通知してくるけれど、指を離して位置が確定したときだけ教えてくれればいい。

element.position
  .sample(element.pressed.filter{ !$0 })
  .subscribe(onNext: { position in
    // 指を離して位置が決定したときに行う処理
  })
  .disposed(by: disposeBag)

sample の引数に渡すトリガーとして、pressed が false になったとき(指を離した時)を指定しています。つまり指を離したときの直近の position の値がイベントとして通知されます。

sample2.png

sample ではトリガーが発生しても値に変化がなければイベントは発生しません。トリガー発生時に必ず値を通知して欲しい用途では使えません。その場合は withLatestFrom を使います。

withLatestFrom

withLatestFrom は、ある Observable にもう一方の Observabe の最新値を合成します。

combineLatest と違って、イベントの発行タイミングは最初の Observable の発行タイミングと同じです。単にそこにもう一方の最新値を組み合わせます。

トリガーになるイベントの値も利用できて、値に変化がなくてもトリガー発生時に値を通知してくれる sample の拡張版と捉えることもできます。

例えばエラー発生時に、発生したエラーとエラー発生時の現在地の緯度経度から、新たなエラーを生成して通知する Observable を作ってみます。

  var errorEvent: Observable<ErrorType> {
    return hoge.errorEvent
      .withLatestFrom(locationMonitor.location) {
        error, location in MyError.hogeError(error, location)
      }
  }

withLatestFrom.png

元の Observable のイベントを利用しないなら、第二引数のクロージャは省略できます。sample だと値変化がないと通知してくれないんだよな〜それは困るなぁ〜ってときはこちらを使います。

element.pressed
  .filter { !$0 }
  .withLatestFrom(element.position)
  .subscribe(onNext: { position in
    // 指を離して位置が決定したときに行う処理
  })
  .disposed(by: disposeBag)

Observableからの現在値の取得

BehaviorSubject やそれを内部で利用している Variable は現在値を取得するメソッド/プロパティを持っていますが、これを Observable として公開した場合、外部からはどうやって現在値を取得するのでしょうか?

単純な考えとしては、subscribe して即 dispose すればいいだけです。現在値がない場合は、イベントが発生せずに即 dispose するので何も起こりません。

running.subscribe(onNext: { running in
  // ここにrunningの現在値を使った処理を書く
  if running {
    // ...
  } else {
    // ...
  }
}).dispose()

自分はこんな拡張を用意して使っています。

import RxSwift

extension Observable {
  /// 要素の最初の1つを適用して処理を実行する
  ///
  /// (Variable含む)BehaviorSubject利用のObservableの現在値を適用するのに利用できる。
  /// 注;PublishSubject利用のObservableでは何も起こらない。
  func applyFirst(handler: (E) -> Void) {
    take(1).subscribe(onNext: handler).dispose()
  }

  /// 最初の値を取得する。
  ///
  /// 注; 最初の値を持っていない場合はnilを返す。
  var firstValue: E? {
    var v: E?
    applyFirst { v = $0 }
    return v
  }

  /// 現在値を取得する(firstValueのエイリアス)
  ///
  /// (Variable含む)BehaviorSubject利用のObservableの現在値を取得するのに利用できる。
  var value: E? { return firstValue }
}

take(1) する applyFirst を用意しているのは、Rx の Observable の中には subscribe した時点で2つ以上のイベントを送ってくるものもあるからです。PublishSubject, BehaviorSubject しか使わないなら気にしなくてもいいんですけど、この方が行儀がいいかなと。ちなみに take(1) するから onCompleted が来ると思って dispose しないでいると、PublishSubject の場合に最初のイベントが来るまで待ち続けることになります。

なお本来の Rx の概念であるリアクティブプログラミングを徹底して行うなら、Observable から現在値を取得したいという状況は起こらないはずです。でもまずは KVO や delegate の置き換えとして使ってみようって場合は、やっぱり現在値が欲しい場面はでてきます。

加工時のObservable生成の無駄をなくす

加工・合成すると新たな Observable が生成されます。

class Presenter {
  private let action: Action

  var startButtonEnabled: Observable<Bool> {
    return action.targetName.map { !$0.isEmpty }
  }
//...

上記の場合、startButtonEnabled を取得するたびに新しい Observable が生成されてしまいます。同じ処理をする Observable を複数生成することになって無駄です。

これを回避するには、computed property にしないでコンストラクタで代入すればいいんです。

class Presenter {
  let startButtonEnabled: Observable<Bool>

  init(action: Action) {
    startButtonEnabled = action.targetName.map { !$0.isEmpty }
  }
//...

これなら生成された1つの Observable が startButtonEnabled に保持されるので、何度取得されても同じインスタンスが返ります。

でも宣言場所と定義場所が分かれててさっきよりコードのメンテナンス性が下がるなぁって思いません?上の例は短いからいいですが、プロパティが増えるとコンストラクタが膨れ上がったりして、やっぱり以下のように書けるといいなぁと思うんです。

class Presenter {
  private let action: Action

  let startButtonEnabled = action.targetName.map { !$0.isEmpty }

  init(action: Action) {
    self.action = action
  }
//...

でもコンストラクタで代入するものは、宣言時には使えません。上のは action が使えずコンパイルエラーになります。

ちょっと記述が面倒になりますが、lazy を使うと宣言と定義を同じ場所にして、しかも Observable インスタンスを共通化することができます。

class Presenter {
  private let action: Action

  lazy var startButtonEnabled: Observable<Bool> = { [unowned self] in
    return self.action.targetName.map{ !$0.isEmpty }
  }()
//...

初めてプロパティにアクセスしたときに Observable が生成されて、以降は同じインスタンスが返ります。

let にコンストラクタで代入する方は、常に同じものが返ることが文法的に保証されます。実装上同じものを返してるんだから問題ないと捉えるかどうか(私は問題ないと思っています)。Subject を Observable として公開する場合も同様で、let にコンストラクタで代入する方法も取れます。

shareReplay(1)またはshare()の付加

さて先ほどの方法で同じ処理をする Observable インスタンスを複数生成しないようにはしましたが、これを複数から subscribe すると、全く同じ変換・合成処理が別々に実行されてしまいます。

これが何故なのか理解するには Rx の Hot と Cold の理解が必要なのですが、Rx の分かりにくい部分でもあります。

とりあえず今は、加工・合成したらおまじないとして最後に shareReplay(1) または share () を付けると覚えておけば十分です。

源流が BehaviorSubject で、subscribe する度に直近の値を流して欲しいなら shareReplay(1) を付けます。メソッド名が長くてもいいなら、引数が1の場合に最適化された shareReplayLatestWhileConnected を使ってもいいです。

  lazy var startButtonEnabled: Observable<Bool> = { [unowned self] in
    return self.action.targetName
      .map { !$0.isEmpty }
      .shareReplayLatestWhileConnected()
  }()

源流が PublishSubject で、イベントを一切キャッシュしないなら share() を付けます。

  var errorEvent: Observable<ErrorType> {
    return Observable.of(hoge.errorEvent, fuga.errorEvent).merge().share()
  }

加工時には副作用は記述しない

イベントの加工・合成処理には副作用を記述しないのが暗黙のルールです。例えば以下のように書くことは可能ですが、Rx の流儀に反します。

  lazy var startButtonEnabled: Observable<Bool> = { [unowned self] in
    return self.action.targetName
      .map {
       NSLog("targetName: \($0)")
       return !$0.isEmpty
      }
      .shareReplayLatestWhileConnected()
  }()

副作用は基本的に subscribe して実行しますが、もしストリーム加工の途中でどうしても副作用を記述したいなら do メソッドが利用できます(ver 2.x 系では doOn でした。また doOnNext/doOnError/doOnCompletedが使えました。)

  lazy var startButtonEnabled: Observable<Bool> = { [unowned self] in
    return self.action.targetName
      .do(onNext: { NSLog("targetName: \($0)") })
      .map { !$0.isEmpty }
      .shareReplayLatestWhileConnected()
  }()

注意点として、do に記述したものはどこからか subscribe されない限り実行されません。上の例で targetName に変化があっても、誰も subscribe していなければログは出力されません。

PublishとBehaviorをどうやって区別するか?

subscribe したときに直近の値が流れてくる BehaviorSubject タイプなのか、そうでない PublishSubject タイプなのかは、どちらも型は単なる Obeservable なのでインターフェース上では区別がつきません。

どちらのタイプなのかは実装を見て確認してね・・・なんてのはカプセル化の観点から勘弁して欲しい・・・となると、

  1. コメントに書く
  2. 名前で区別できるようにする

のどちらかかなぁと。コメントに書いた内容は Xcode 上で表示されるので、1 も手かと思いますが、私は 2 の方法を選択しました。

  • PublishSubject タイプならプロパティ名の末尾に Event を付ける
  • BehaviorSubject タイプは KVO 対応の普通のプロパティと同じ名前付けルール

というようにしています。

RxCocoa

RxSwift の一部として、iOS の既存クラスを拡張する RxCocoa が含まれています。例えば UIButton には rx.tap というプロパティが拡張されていて、タップイベントを subscribe で受け取れます。

startButton.rx.tap
  .subscribe(onNext: { [unowned self] in self.presenter.start() })
  .disposed(by: disposeBag)

他にも bindTo に渡せる先述した rx.isHidden や、UIControl の拡張プロパティ rx.isEnabled など、UIKit を Rx で扱うのに便利な拡張が多数用意されています。

UIKit 拡張の詳細は RxCocoa/iOS のコードを参照してください。また iOS / Mac OS 共通クラスの拡張は RxCocoa/Common のコードを参照してください。

Rxの威力を享受できる設計

Rx の利点を活かすための設計方針として、各クラスの入力を戻り値のないメソッド呼び出しにして、出力を Observable にすると幸せになれます。もちろん適宜適切と思えるところは変えるべきですが、基本方針をこのように設計すると Rx の強力さを実感できるかと思います。

クラス間の依存関係は相互参照、循環参照がないようにするのが基本です。以下では MainViewController が MainPresenter を参照していますが、逆の参照はありません。

ViewController_Presenter.png

MainViewController は MainPresenter にイベントを伝えるのに、相手のメソッドを呼び出します。一方 MainPresenter は MainViewController にイベントを伝えるのに、Observable をプロパティで公開します。MainViewController がこれら Observable を購読することで、イベントが伝わるようになります。

初めて Rx を使うなら、KVO や delegate を置き換えるつもりでこの設計を採用することをお勧めします。

入力もメソッドでなく Observer として受け取ってフルに Rx を利用する設計については、「Rxを使った設計をビジュアル化する」で詳しく解説します。

この先へ

オブサーバーパターンを置き換えるところから始め、 KVO を置き換える方法も示しつつ、Rx の最大の特徴であるイベントシーケンスの加工・合成方法を説明しました。

これだけでも使えるようになれば、十分便利だと思います。

今回解説したのは Rx の Hot と Cold のうち、全てを Hot として扱う方法です。次の記事「RxSwift入門(2) 非同期処理してみる」では、Cold な Observable の生成方法や、Hot / Cold とは何なのか、また非同期処理を行うための実行スレッドの指定方法などを紹介します。


  1. RxJavaではSubscriptionクラスでunsubscribeメソッドで購読解除します。コンセプトは同じですが、実装によって名前が違っていたりします。 

  2. DisposeBagはRxSwift独自のものです。似たようなのはRxJavaにもCompositeSubscriptionというのがありますが、デストラクタで自動解放というのはガーベジコレクタを使わない言語ならではです。 

  3. 例えばRxJavaではObservable.merge(observable1,observable2)というクラスメソッドが用意されています。 

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
820