Search code examples
iosswiftrx-swiftreactivexrx-cocoa

RxSwift simple withLatestFrom not firing


I have an observable

lazy var user = _user.share(replay: 1)
private let _user = PublishRelay<User>()

in a UserState.swift singleton. My user observable gets an event as soon as the app loads and I receive a User from the backend.

Later on, in one of my ViewModels, I try to

func profileViewModel(
    viewAppeared: Observable<Void>
) -> (
    title: Observable<String>
) {

    let title = viewAppeared.withLatestFrom(UserState.shared.user) { _, user in
        user.username
    }

    return title

}

Except this event never fires (dont get inside the closure) because user already fired before it was subscribed to.

How can I accomplish this behavior where on viewDidLoad event, I grab the latest user value from my observable?

EDIT:

In my singleton init, if I throw in a random user.subscribe()..., then this all works. I'm so confused...


Solution

  • I see the problem. The PublishRelay is basically just a PublishSubject.

    Anything pushed onto it prior to your viewModel's subscription is going to be lost. The .share(replay: 1) would have helped, except that something needs to be subscribed to that before the user is pushed onto it in order for it to "catch" the event and cache the value for later subscribers.

    And even then you probably would need .share(replay: 1, scope: .forever).

    You need to remember that you can setup all of the observable chains that you want, but no operations in that chain will execute until something eventually subscribes to that chain.

    Change your user state to a BehaviorSubject<User?>(value: nil). That will capture and HOLD the current user if and when a user is put onto it, even if nothing has subscribed to it.

    Then do something like the following on your view model.

    class ProfileViewModel {
    lazy var user = UserState.shared.user.compactMap { $0 }
    lazy var name = user.map { $0.username }
    lazy var age = user.map { "\($0.age) years old" }
       .do(onNext: { print($0) }
    // etc
    }
    
    class profileViewController {
      ...
      override func viewDidLoad() {
        ...
        profileViewModel.name
          .subscribe(onNext: { [weak self] name in 
            self?.title = name
          }
          .disposed(by: disposeBag)
      }
    }
    

    Note that you want this if the user (and thus the username) can change dynamically during the period this view controller is displayed and you want the title updated automatically.

    Also note that, as shown, you'll NEVER see the user's age printed in the .do statement, as currently nothing is subscribing or binding to that observable sub-chain.