LoginSignup
24

More than 5 years have passed since last update.

RxSwiftのshareを利用する際の意外と気づきにくい挙動の違い

Last updated at Posted at 2017-12-07

はじめに

AbemaTV Advent Calendar 2017 5日目の記事です。(5日目過ぎてますが、12/5だけ空いてたので書いてしまいますw)
前日の4日目の記事は@splas_boomerangさんの「Googleが規定する品質要件から見るDaydreamプラットフォームの特徴」で、翌日の6日目の記事は@yasuhideshimizuさんの「VRのUI作りにおすすめのsketchplugin『Sketch to VR』」になります。

RxSwiftでObservableのHot変換する際に、.share().share(replay:scope:)を利用することがあるかと思います。
本投稿では、share()を利用する際に意外と気づきにくい挙動の違いについて書いていこうと思います。

利用例

下記のように、NetworkConnection.mobile.wifiかによってQualityHandlerが保持するqualityListの内容が変化する実装があるとします。
さらに

  • .mobileの場合にはselectedQualityの値は.low
  • .wifiの場合にはselectedQualityの値は.middle

であるとします。

QualityHandlerは、選択されているNetworkConnectionと選択されるIndexをinitializerで受け取ります。

enum NetworkConnection {
    case mobile
    case wifi
}

enum Quality {
    case unknown
    case low
    case middle
    case high
}

final class QualityHandler {
    var qualityList: [Quality] {
        return _qualityList.value
    }
    var selectedQuality: Quality {
        return _selectedQuality.value
    }

    private let _qualityList = Variable<[Quality]>([])
    private let _selectedQuality = Variable<Quality>(.unknown)
    private let disposeBag = DisposeBag()

    init(selectedNetwork: Observable<NetworkConnection>,
         selectedIndex: Observable<Int>) {
        let wifiConnection = selectedNetwork
            .filter { $0 == .wifi }
            .share() // <-- 今回の注目点

        let mobileConnection = selectedNetwork
            .filter { $0 == .mobile }
            .share() // <-- 今回の注目点

        wifiConnection
            .map { _ -> [Quality] in [.middle, .high] }
            .bind(to: _qualityList)
            .disposed(by: disposeBag)
        mobileConnection
            .map { _ -> [Quality] in [.low, .middle] }
            .bind(to: _qualityList)
            .disposed(by: disposeBag)

        selectedIndex
            .withLatestFrom(_qualityList.asObservable()) { ($0, $1) }
            .flatMap { index, list -> Observable<Quality> in
                guard index < list.count else { return .empty() }
                return .just(list[index])
            }
            .bind(to: _selectedQuality)
            .disposed(by: disposeBag)

        // 選択されているのがwifiだった場合、値をmiddleへ
        wifiConnection
            .withLatestFrom(_selectedQuality.asObservable())
            .filter { $0 == .unknown }
            .map { _ -> Quality in .middle }
            .bind(to: _selectedQuality)
            .disposed(by: disposeBag)

        // 選択されているのがmobileだった場合、値をlowへ
        mobileConnection
            .withLatestFrom(_selectedQuality.asObservable())
            .filter { $0 == .unknown }
            .map { _ -> Quality in .low }
            .bind(to: _selectedQuality)
            .disposed(by: disposeBag)
    }
}

selectedNetworkがPublishSubjectの場合

1つ目のprintでは、またselectedNetworkに値が流れていないため、unknownとなります。qualityListも空になります。
2つ目のprintでは、直前に.wifiを流しているので、middleとなります。qualityListには[.middle, .high]が入っています。
3つ目のprintでは、直前にindex1を流しているので、highとなります。
それぞれ、期待していた値がprintで表示されています。

let selectedNetwork = PublishSubject<NetworkConnection>()
let selectedIndex = PublishSubject<Int>()

let handler = QualityHandler(selectedNetwork: selectedNetwork, selectedIndex: selectedIndex)

handler.qualityList.forEach { print($0) } // 何も表示されない
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = unknown

selectedNetwork.onNext(.wifi)
handler.qualityList.forEach { print($0) } // middle high
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = middle

selectedIndex.onNext(1)
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = high

selectedNetworkがVariableの場合

Variableであるため、QualityHandlerの初期化時に.wifiの値が即時に流れます。
1つ目のprintでは、middleになっていてほしいところですが、初期値のunknownのままになっています。
ところが、qualityListには[.middle, .high]が入っています。
2つ目のprintでは、直前でもう一度.wifiを入れ直しているので、値が流れてmiddleに変わります。
3つ目のprintでは、直前にindex1を流しているので、highとなります。
1つ目が、期待していたものとは違っています。

let selectedNetwork = Variable<NetworkConnection>(.wifi)
let selectedIndex = PublishSubject<Int>()

let handler = QualityHandler(selectedNetwork: selectedNetwork.asObservable(),
                            selectedIndex: selectedIndex)

handler.qualityList.forEach { print($0) } // middle high
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = unknown

selectedNetwork.value = .wifi
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = middle

selectedIndex.onNext(1)
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = high

selectedNetworkがObservable.justの場合

Observable.just.wifiの値が即時に流れます。
1つ目のprintでは、middleになっていてほしいところですが、初期値のunknownのままになっています。
ところが、qualityListには[.middle, .high]が入っています。
2つ目のprintでは、直前にindex1を流しているので、highとなります。
1つ目が、期待していたものとは違っています。

let selectedNetwork = Observable<NetworkConnection>.just(.wifi)
let selectedIndex = PublishSubject<Int>()

let handler = QualityHandler(selectedNetwork: selectedNetwork, selectedIndex: selectedIndex)

handler.qualityList.forEach { print($0) } // middle high
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = unknown

selectedIndex.onNext(1)
print("selectedQuality = \(handler.selectedQuality)") // selectedQuality = high

まとめ

上記の違いとしては、QualityHandlerの初期化時に即時に値が流れるかそうでないかの違いがあります。

QualityHandlerのinit内の

let wifiConnection = selectedNetwork
    .filter { $0 == .wifi }
    .share()

に一旦フォーカスしてみます。

wifiConnection

wifiConnection
    .map { _ -> [Quality] in [.middle, .high] }
    .bind(to: _qualityList)
    .disposed(by: disposeBag)

...

wifiConnection
    .withLatestFrom(_selectedQuality.asObservable())
    .filter { $0 == .unknown }
    .map { _ -> Quality in .middle }
    .bind(to: _selectedQuality)
    .disposed(by: disposeBag)

の2箇所でsubscribe(bind)されています。

selectedNetworkがVariableの場合の1つ目のprintはunknownになっていましたが、qualityListには適切な値が代入されていました。
上記の実装からもわかるように、qualityListに値が代入されているのは1つ目のsubscribeになります。
.share()したObservableの値が上流から即時に流れる場合、2つ目以降ののsubscribeが完了する前に1つ目のsubscribeに対して値が流れてしまっているので、期待していた動きになっていない状態となります。
そのため、.share()ではなく.share(replay:scope:)を利用すれば、内部で値をキャッシュしているので、期待していた動きになります。
値が即時に流れるかどうかはメソッドの引数がObservableの状態ではわからないので、上流を見て.share()を使うのか.share(replay:scope:)かを見極めていく必要がありそうです。

追記

また、下記のように値を流す用のPublishSubjectを用意し、全てのsubscribeが完了した後にbindすることで.share()を利用したままの実装も可能です。

final class QualityHandler {

    ...

    init(selectedNetwork: Observable<NetworkConnection>,
         selectedIndex: Observable<Int>) {
        let _selectedNetwork = PublishSubject<NetworkConnection>() // 最後に値を流す用

        let wifiConnection = _selectedNetwork
            .filter { $0 == .wifi }
            .share() // <-- 今回の注目点

        let mobileConnection = _selectedNetwork
            .filter { $0 == .mobile }
            .share() // <-- 今回の注目点

        wifiConnection
            .map { _ -> [Quality] in [.middle, .high] }
            .bind(to: _qualityList)
            .disposed(by: disposeBag)

        ...

        // 選択されているのが.mobileだった場合、値をlowへ
        mobileConnection
            .withLatestFrom(_selectedQuality.asObservable())
            .filter { $0 == .unknown }
            .map { _ -> Quality in .low }
            .bind(to: _selectedQuality)
            .disposed(by: disposeBag)

        selectedNetwork // 全てのsubscribeが終わったあとに、bindする
            .bind(to: _selectedNetwork)
            .disposed(by: disposeBag)
    }
}

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
24