ReactiveSwift もいいぞという話

Swift のリアクティブプログラミングのライブラリといえば RxSwiftReactiveSwift が有名ですよね。

今まで RxSwift しか使ってこなかったのですが ReactiveSwift を使ってみたらとても良かったのでそれについて書きたいと思います。

ReactiveSwift と RxSwift を比較する形で紹介します。

RxSwift やその他 Rx ライブラリをある程度自由に使いこなせるという方を対象に書いています。

ライブラリの構成

ReactiveSwift RxSwift
for Swift ReactiveSwift RxSwift
for Cocoa ReactiveCocoa RxCocoa
依存ライブラリ Result

ReactiveSwift の構成は RxSwift と似ています。
xxxSwift がピュア Swift 用
xxxCocoa が Cocoa 用
のフレームワークです。

1点違うのは ReactiveSwift は Result というライブラリに依存していることです。
なので ReactiveSwift を使うと自動的に Result がついてきます。

「えー依存ライブラリ増えんのかよ」と思われるかもしれませんが Result はとてもいいライブラリですし、Result があるからこそ ReactiveSwift が便利なのです。

だから大丈夫です👍🏼

ReactiveSwift のここが素晴らしい

Hot / Cold の区別が型から判別できる

ReactiveSwift では ObservableHotCold かを型で判別できます。

ReactiveSwift RxSwift
Hot Signal<Int, MyError> Observable<Int>
Cold SignalProducer<Int, MyError> Observable<Int>

ずっと RxSwift に慣れていたので Hot / Cold の区別がつくことがどれだけ嬉しいか、正直未知数でしたが使ってみるととても便利です。

RxSwift を使っていると「この Obsevable って HotCold?どっちやったっけ」と考える必要がありますがそれがなくなります。

こういった暗黙の了解がなくなるのは嬉しいですね。

Hot / Cold の違いについては @ukitaka さんの 今日こそ理解するHot / Cold が分かりやすいです。

Error の型を指定できる

Signal / SignalProducer第二型パラメータに Error の型 を渡すようになっています。

ReactiveSwift RxSwift
Hot Signal<Int, MyError> Observable<Int>
Cold SignalProducer<Int, MyError> Observable<Int>

RxSwift は エラーが Error プロトコルとしてしか取り出せない ので、場合によってはキャストする必要があります。

// RxSwift の場合
let myObservable: Observable<Int> = ...

myObservable
    .subscribe(onError: { error in
        guard let myError = error as? MyError {
            // ここに来ることはないという"暗黙の了解"が発生する
            return
        }
        // 何かする
    })
    .disposed(by: disposeBag)

一方で ReactiveSwift ではエラーの型を明示的に表すので余計なキャストをすること必要はありません。

// ReactiveSwift の場合
let myProducer: SignalProducer<Int, MyError> = ...

myProducer.startWithFailed { (myError: MyError) in
    // myError は MyError 型ということがコンパイル時に保証される!
}

「Error の型が分かっててもどうせ localizedDescription しか使わんしな〜」 という方もいらっしゃるかもしれません。
👇🏼しかしこちらをご覧ください。

// エラーの型が NoError というところがポイント👇🏼
let noErrorProducer: SignalProducer<Int, NoError> = ...

Signal / SignalProducer のエラー型に NoError を指定するとその名の通り エラーが起きないということを表現できます。

エラーが起きないという 暗黙の了解を型で表現できる のが ReactiveSwift の嬉しいところですね。

NoError について

ところで「NoError て急に出てきたけど何?」と疑問を抱いた方もいらっしゃると思います。
ちょっと定義を見てみましょう

/// An “error” that is impossible to construct.
///
/// This can be used to describe `Result`s where failures will never
/// be generated. For example, `Result<Int, NoError>` describes a result that
/// contains an `Int`eger and is guaranteed never to be a `failure`.
public enum NoError: Swift.Error, Equatable {
    public static func ==(lhs: NoError, rhs: NoError) -> Bool {
        return true
    }
}

NoError の実態は enum です。しかし case を一つも持たないので インスタンス化できません
これを利用してエラーが発生しないということを表現しています。

Action が便利

ReactiveSwift には Action というクラスがあります。これもまた便利です。

Action を使うと

1. ボタンタップ
2. API 通信
3. 成功時: 画面遷移 / 失敗時: アラートを表示
※ただしタップ連打などで通信が二重に走らないように制御する

といった、よくあるパターンが簡単に実装できます。

ただ説明するのが難しすぎるのでまずサンプルコードをご覧ください。
わかりにくかったらごめんなさい。多分わかりにくいので別の記事で詳しく紹介したいです。

サンプルコード

// わかりやすくするためになるべく型を省略せずに書いています
class APIClient {

    func searchUsers(name: String) -> SignalProducer<[User], APIError> {
        return ...
    }
}

let apiClient = APIClient()

// 👇🏼ここで定義しているのが Action
// ここでは String をトリガーにして
// searchUsers を呼び出す Action を定義しています
let action = Action<String, [User], APIError> { input in
    return apiClient.searchUsers(name: input) // ②
}

// 👇🏼 searchUsers の値を購読
action.values.observeValues { (users: [User]) in
    print(users) // ③-A
}

// 👇🏼 searchUsers のエラーを購読
action.errors.observeValues { (error: APIError) in
    print(error) // ③-B
}

// 👇🏼 "John" をトリガーに action を実行
action.apply("John").start() // ①

① → ② → ③-A or ③-B の順番で実行されます。

Action の嬉しいところ

1. 一度に実行できる Action は 1 つまで

このおかげで ボタンタップ→通信 というよくあるフローを実装するときに、連打されて通信が二重に走るということが無くなります。

2. エラーが発生しても購読が終了しない

Signal / SignalProducer はエラーが発生するとストリームが終了してしまいますよね。
Action を介すると エラーの発生を onError ではなく onNext として流してくれます。

なのでエラーが発生してストリームが終了するという事がなくなります。

3. Action が実行中かどうか判定できる

Action には let isExecuting: Property というプロパティが生えています。
その名の通り Action実行中かどうかを表すプロパティ です。

こいつを使えば 通信中はローディングを表示する といったよくあるパターンを簡単に実装できます。

let activityIndicator = UIActivityIndicatorView()
activityIndicator.reactive.isAnimating <~ action.isExecuting

最初理解するまで苦労しました Action はとても便利です。

ちなみに RxSwift にも Action の概念は存在しています。
RxSwiftCommunity/Action

RxSwift をやっている方で「ようわからん!」という方は下の記事がわかりやすいです。
Actionを使って快適なViewModel生活を

最終的に「RxSwift でいいや…」となったとしても Action の存在はぜひ知っておきたいところです

CocoaAction も便利

CocoaAction はボタンのタップなどの UI のイベントと Action を仲介してくれるクラスです。

サンプルコード

// 👇🏼 さっき作った Action
let action: Action<String, [User], APIError> = ...

button.reactive.pressed = CocoaAction(action) {
  textField.text ?? "" // タップ(=Void) を String へ変換し action への入力とする
}

たったこれだけのコードで
ボタンのタップが action への入力に変換されて通信のトリガーになる
action の処理が実行中は button の isEnabled が false になる

ということを実現できます。

よくある実装パターンで当然 RxSwift でもできるのですが Action / CocoaAction を使うとよりスッキリかけます。

ReactiveSwift と RxSwift の対応

RxSwift から ReactiveSwift に乗り換える時に覚えておきたいのが RxSwift と ReactiveSwift のインターフェースの差です。

ここでは一部ですが ReactiveSwift と RxSwift の対応をご紹介します。

ReactiveSwift にも RxSwift にも存在するもの

インターフェースが同じもの

map, withLatestFrom, merge, combineLatest…など

インターフェースが異なるもの

PublishSubject / Signal.pipe

RxSwift でいう PublishSubject は ReactiveSwift の Signal.pipe だと個人的に思っています。(合ってる?)

// RxSwift
let subject = PublishSubject<Int>()

subject
  .bind {
    print($0)
  }
  .disposed(by: disposeBag)

subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
// ReactiveSwift
let (output, input) = Signal<Int, NoError>.pipe()

output.observeValues {
  print($0)
}

input.send(value: 1)
input.send(value: 2)
input.send(value: 3)

Signal<Int, NoError>.pipe() は input と output がそれぞれ分かれています。

Observable.create / SignalProducer.init

ColdObsevable の作成の違いです。

// RxSwift
let observable = Observable<Int>.create { observer in

  observer.onNext(1)
  observer.onNext(2)
  observer.onNext(3)
  observer.onCompleted()

  return Disposables.create()
}
// ReactiveSwift
let producer = SignalProducer<Int, NoError> { observer, _ in
  observer.send(value: 1)
  observer.send(value: 2)
  observer.send(value: 3)
  observer.sendCompleted()
}
flatMap

RxSwift の flatMap / flatMapLatest / concat / amb は ReactiveSwift では flatMap にまとめられています。

flatMap の第一引数に FlattenStrategy を渡すことで挙動を変えます。

in RxSwift in ReactiveSwift
flatMap FlattenStrategy.merge
flatMapLatest FlattenStrategy.latest
concat FlattenStrategy.concat
amb FlattenStrategy.race
FlattenStrategy.concurrent(limit: UInt)

こんな感じで第一引数に FlattenStrategy を渡して呼び出します。

producer.flatMap(FlattenStrategy.latest) { ... }

FlattenStrategy.concurrent は ReactiveSwift にだけあるものです。
concat の同時起動数を指定できます。

flatMapFirst 的なオペレータはまだないようです。こちらの issue で作られそうな雰囲気があります。
[Idea] Add FlattenStrategy.throttle

BehaviorRelay / MutableProperty(と Property)

RxSwift の BehaviorRelay(旧 Variable) に対応する ReactiveSwift のクラスは MutableProperty です。

let count: MutableProperty<Int> = MutableProperty(0)

// 値の更新
state.value = 1

また Property というクラスもありこちらの valueimmutable です。
読み取り専用の BehaviorRelay という感じです。
RxSwift にはなかったので便利ですね。

let state: Property<Int> = Property(0)
// 値の更新はできない❌
state.value = 1

MutableProperty から Property へも変換できます。

// MutableProperty から Property への変換
let mutableProperty = MutableProperty(0)
let readOnlyProperty = Property(mutableProperty) // or Property(capturing: mutableProperty)

MutableProperty or Property を別の Property に変換するメソッドもあります。

let userName = MutableProperty("")
let isEnabled: Property<Bool> = userName.map { !$0.isEmpty }

ちなみに BehaviorRelay は RxCocoa に定義されていますが
Property / MutableProperty は ReactiveSwift に定義されています。

distinctUntilChanged / skipRepeats

これらは名前が違うだけで同じです。

ReactiveSwift にしか存在しないもの

全部あげるとキリがないので面白いなと思ったものをピックアップします。

skipNil

その名の通り nil をスキップします。スキップすると同時にアンラップもしてくれます。

let optional: SignalProducer<Int?, NoError> = ...
let skipNil: SignalProducer<Int, NoError> = optional.skipNil() // ← アンラップされている

combinePrevious

直前の値を取得するオペレータです。

let values: SignalProducer<Int, NoError> = ...
values
  .combinePrevious()
  .map { previous, current in
    // 何かする
  }

RxSwift でもオペレータを組み合わせたら可能です。
Rxで1つ前の値を取得する

RxSwift にしか存在しないもの

こちらも一部だけです。

Driver, Single, Maybe, Completable

ReactiveSwift にはこれらに対応する型がありません。
ただ、現時点ではそんなに困ってないです。

RxCocoa の DelegateProxy

RxCocoa には UIKit の delegate や dataSource などの Rx 拡張があります。
delegate を実装しなくてもいいので便利です。

// UIScrollViewDelegate の didScroll メソッドの Rx 拡張
scrollView.rx.didScroll.bind {
    print("didScroll")
}

しかし ReactiveCocoa にはありません。
この点は RxSwift いいなあと思います。

まとめ

ReactiveSwift では RxSwift における暗黙の了解を型として表せるのが一番嬉しいポイントだと思います。
「RxSwift 流行ってるしやってみるか」となる方が多いと思うのですが ReactiveSwift も便利なので RxSwift と ReactiveCocoa 比べてみてからどちらを使うか検討してもいいのではないでしょうか。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です