iOS
Swift
RxSwift

RxSwiftでwithLatestFromが最新じゃなくなることがあるorz

Observable①でwithLatestFromをしてObservable②を取り込むとします。
Observable①の上流でObservable②がトリガーだった場合に、想定しいない値が入ってくる場合があります。

想定している最新値が取得できない実装

下記のような実装があるとします。

  1. selectedUserの値が更新され、その値がnilでなかった場合にshowUserでvoidを流す
  2. showUserに値が流れたら、withLatestFromで最新のselectedUserを取得
  3. withLatetFromで取得したselectedUserをprint
ソース①
let selectedUser = Variable<String?>(nil)
let showUser = PublishSubject<Void>()

_ = selectedUser.asObservable()
    .filter { $0 != nil }
    .debug("selectedUser")
    .subscribe(onNext: { _ in
        showUser.onNext()
    })

_ = showUser
    .withLatestFrom(selectedUser.asObservable().debug("withLatestFrom.selectedUser"))
    .debug("showUser")
    .subscribe(onNext: { user in
        print("print user = ", user)
    })

print("set tatusmi")
selectedUser.value = "tatsumi"

さて、上記の実装の場合、どのようなものがprintで表示されるでしょうか。
showUserのトリガーがnilではないselectedUserになっているので、print user = tatsumiになることを想定しています。
しかしながら、実際にはprint user = nilとなります。
showUserのトリガーがselectedUserに値が流れてnil出ないことを確認しているにも関わらず、withLatestFromで取得したselectedUserがnilになってしまっています。

デバッグログは下記のようになります。

2017-08-24 14:51:32.339: selectedUser -> subscribed
2017-08-24 14:51:32.342: showUser -> subscribed
2017-08-24 14:51:32.342: withLatestFrom.selectedUser -> subscribed
2017-08-24 14:51:32.342: withLatestFrom.selectedUser -> Event next(nil)
set tatusmi
2017-08-24 14:51:32.343: selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 14:51:32.343: showUser -> Event next(nil)
print user =  nil
2017-08-24 14:51:32.343: withLatestFrom.selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 14:51:32.344: selectedUser -> Event completed
2017-08-24 14:51:32.344: selectedUser -> isDisposed
2017-08-24 14:51:32.344: withLatestFrom.selectedUser -> Event completed
2017-08-24 14:51:32.344: withLatestFrom.selectedUser -> isDisposed

ソース①では

  1. selectedUser
  2. showUser
  3. withLatestFrom.selectedUser

という順番にsubscribeされています。

subscribe後は

  1. withLatestFrom.selectedUserの直後にwithLatestFrom.selectedUserで初期値のnilをキャッシュ
  2. selectedUserにtatsumiが流れてくる
  3. showUserに値が流れてくる
  4. printを実行
  5. withLatestFrom.selectedUserにtatsumiが流れてくる

という流れで実行されます。

想定している最新値が取得できる実装①

それでは、showUserを間に挟まない実装をした場合はどうでしょうか。

ソース②
let selectedUser = Variable<String?>(nil)

_ = selectedUser.asObservable()
    .withLatestFrom(selectedUser.asObservable().debug("withLatestFrom.selectedUser"))
    .filter { $0 != nil }
    .debug("selectedUser")
    .subscribe(onNext: { user in
        print("print user = ", user)
    })

print("set tatusmi")
selectedUser.value = "tatsumi"

実行結果はprint user = tatsumiとなります。

2017-08-24 14:52:02.103: selectedUser -> subscribed
2017-08-24 14:52:02.103: withLatestFrom.selectedUser -> subscribed
2017-08-24 14:52:02.104: withLatestFrom.selectedUser -> Event next(nil)
set tatusmi
2017-08-24 14:52:02.105: withLatestFrom.selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 14:52:02.105: selectedUser -> Event next(Optional("tatsumi"))
print user =  Optional("tatsumi")
2017-08-24 14:52:02.106: withLatestFrom.selectedUser -> Event completed
2017-08-24 14:52:02.106: withLatestFrom.selectedUser -> isDisposed
2017-08-24 14:52:02.106: selectedUser -> Event completed
2017-08-24 14:52:02.107: selectedUser -> isDisposed

ソース②では

  1. selectedUser
  2. withLatestFromのselectedUser

という順番にsubscribeされています。

subscribe後は

  1. withLatestFrom.selectedUserの直後にwithLatestFrom.selectedUserで初期値のnilをキャッシュ
  2. withLatestFrom.selectedUserにtatsumiが流れてくる
  3. selectedUserにtatsumiが流れてくる
  4. showUserに値が流れてくる
  5. printを実行

という流れで実行されます。

想定している最新値が取得できる実装②

ソース③
let selectedUser = Variable<String?>(nil)
let showUser = PublishSubject<Void>()

_ = showUser
    .withLatestFrom(selectedUser.asObservable().debug("withLatestFrom.selectedUser"))
    .debug("showUser")
    .subscribe(onNext: { user in
        print("print user = ", user)
    })

_ = selectedUser.asObservable()
    .filter { $0 != nil }
    .debug("selectedUser")
    .subscribe(onNext: { _ in
        showUser.onNext()
    })

print("set tatusmi")
selectedUser.value = "tatsumi"

実行結果はprint user = tatsumiとなります。

2017-08-24 14:50:42.238: showUser -> subscribed
2017-08-24 14:50:42.239: withLatestFrom.selectedUser -> subscribed
2017-08-24 14:50:42.239: withLatestFrom.selectedUser -> Event next(nil)
2017-08-24 14:50:42.241: selectedUser -> subscribed
set tatusmi
2017-08-24 14:50:42.242: withLatestFrom.selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 14:50:42.242: selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 14:50:42.242: showUser -> Event next(Optional("tatsumi"))
print user =  Optional("tatsumi")
2017-08-24 14:50:42.243: withLatestFrom.selectedUser -> Event completed
2017-08-24 14:50:42.243: withLatestFrom.selectedUser -> isDisposed
2017-08-24 14:50:42.243: selectedUser -> Event completed
2017-08-24 14:50:42.243: selectedUser -> isDisposed

ソース③では

  1. showUser
  2. withLatestFromのselectedUser
  3. selectedUser

という順番にsubscribeされています。

subscribe後は

  1. withLatestFrom.selectedUserのsubscribe直後にwithLatestFrom.selectedUserで初期値のnilをキャッシュ
  2. withLatestFrom.selectedUserにtatsumiが流れてくる
  3. selectedUserにtatsumiが流れてくる
  4. showUserに値が流れてくる
  5. printを実行

という流れで実行されます。

その他の実装

withLatestFromではなく、flatMapでselectedUserを都度取り込みます。

ソース④
let selectedUser = Variable<String?>(nil)
let showUser = PublishSubject<Void>()

_ = selectedUser.asObservable()
    .filter { $0 != nil }
    .debug("selectedUser")
    .subscribe(onNext: { _ in
        showUser.onNext()
    })

_ = showUser
    .flatMap { selectedUser.asObservable().debug("withLatestFrom.selectedUser") }
    .debug("showUser")
    .subscribe(onNext: { user in
       print("print user = ", user)
    })

print("set tatusmi")
selectedUser.value = "tatsumi"
2017-08-24 15:16:57.120: selectedUser -> subscribed
2017-08-24 15:16:57.122: showUser -> subscribed
set tatusmi
2017-08-24 15:16:57.123: selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 15:16:57.124: withLatestFrom.selectedUser -> subscribed
2017-08-24 15:16:57.124: withLatestFrom.selectedUser -> Event next(Optional("tatsumi"))
2017-08-24 15:16:57.124: showUser -> Event next(Optional("tatsumi"))
print user =  Optional("tatsumi")

最後に

RxSwiftのissueにも似たようなものがありました。
withLatestFrom doesn't actually provide latest value #1121