はじめに
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)
}
}