RxSwiftでsubscribeをネストされると困る


はじめに

RxSwiftでsubscribeをネストされると困るという話を最小限のコードで説明してみる。

例えば次のようなコードがあるとする、これはやってることはシンプルだがこのような書き方を普段しているようなら、処理が複雑になっていくにしたがって読みづらいコードになる。

textField.rx.text
  .subscribe(onNext: { text in
       GitHubRepo.search(from: text).subscribe(onNext: { result in
        ...
       })
      .disposed(by: disposeBag)
})
.disposed(by: disposeBag)

このコードをもう少しシンプルに再現しやすいサンプルでネストしないようにしてみる。

シンプルなネストの例とその解決案

Swift.Sequenceな1, 2, 3を文字列と結合する例を考えてみる。

Observable.of(1, 2, 3)
    .subscribe(onNext: {
        let observable = Observable.of("\($0)A", "\($0)B", "\($0)C")
        observable.subscribe(onNext: {
            print($0)
        })
    })

出力は次の通り

1A
2A
3A

これを解決するため、ネストさせたくなったらまずはflatMap系を思い出してほしい。これも出力は同じになる。

Observable.of(1, 2, 3)
    .flatMap {
        Observable.just("\($0)A")
    }
    .subscribe(onNext: {
        print($0)
    })

しかし、この例がそもそもシンプルすぎるからこんな簡単に置き換えられるわけだ。「1つのシーケンスをまた違う1つのシーケンスにしたいからflatMapで置き換えられているだけじゃないか」と思ったかもしれない。そのため例をもう少しだけ複雑にしてみる。

ほんの少し複雑なネストの例とその解決案

subscribeでログ出力を行わないといけないことを考えた例にしてみる。

Observable.of(1, 2, 3)
    .subscribe(onNext: {
        Logger.log("log1:\($0)")
        let observable = Observable.just("\($0)A")
        observable.subscribe(onNext: {
            Logger.log("log2:\($0)")
            print($0)
        })
    })

// Loggerは適当に...こんなニュアンスだとしてやってください
struct Logger {
    static func log(_ message: String) {
        print(message)
    }
}

このような場合はdoオペレータを使って副作用を分けてほしい

Observable.of(1, 2, 3)
    .do(onNext: {
        Logger.log("log1:\($0)")
    })
    .flatMap {
        Observable.just("\($0)A")
    }
    .do(onNext: {
        Logger.log("log2:\($0)")
    })
    .subscribe(onNext: {
        print($0)
    })

もちろんシーケンスを分岐して別々に処理を書いてもいい。この例ではわざわざ分岐する必要性はないが一応例を示す。

let observable = Observable.of(1, 2, 3)
observable
    .do(onNext: {
        Logger.log("log1:\($0)")
    })

observable
    .flatMap {
        Observable.just("\($0)A")
    }
    .do(onNext: {
        Logger.log("log2:\($0)")
    })
    .subscribe(onNext: {
        print($0)
    })

分岐をする必要性はないが、アイデアの一つとして知っておくと良いと思う。

とにかくシーケンスの副作用の実行をまとめて一箇所のsubscribeでやろうとするからコードが読みづらくなってしまうので、それを避けるために考えを巡らせたい。

おわりに

上記の例はCold Observableを使ってとてもシンプルな例としたが、実際のアプリ開発ではこのようなシンプルなコードではないためコードの処理を追いづらい。他にはSubjectRelayに値をonNextしていたりしてキツイ。一つのシーケンスの処理を追っていたら複数イベントが発火し、それがそこだけの問題なのか判断ができないままコードを追いかけないといけない。

そういうときにこそオペレータを駆使したりRxを調べたりして宣言的にコードを書くことを思い出してほしい。じゃないとRxを使ってるだけの読みづらいコードになってしまうわけですよね。