简体   繁体   中英

RxSwift: Issue with chaining streams

I have a login use case which involves a remote service call and a pin.

In my view model I have a behaviour relay for pin like so

let pin = BehaviorRelay(value: "")

Then I have this service:

protocol LoginService {
    func login(pin: String) -> Single<User>
}

Also in the view model I have a publish relay (to back a submit button) and also a state stream. State must initially set to .inactive and once the submit relay fires I need the state to go .loading and eventually .active .

var state: Observable<State> {
    return Observable.merge(
        .just(.inactive),
        submit.flatMap { [service, pin] in
            service.login(pin: pin.value).asObservable().flatMap { user -> Observable<State> in
                    .just(.active)
            }.catch { error in
                return .just(.inactive)
            }.startWith(.loading)
        })
}

The problem is that should the pin change after submit (and my use case involves clearing the pin once submit button clicked), the service is called a second time with the new pin value (in this case empty string).

I want this stream to just take the value for pin and run the service once only and ignore any new value for pin unless the submit was fired again.

I think you are trying to hard to chain things together :-).
Let's take your problem apart and see if that helps.

The important event for you is the button push. When the user pushes the "submit" button you want to make an attempt to log in.

So attach a subject to your pin entry field and let it capture the result of the user's typing. You want this to be a stream that holds the latest value of the pin:

// bound to a text input field.  Has the latest pin entered
var pin = BehaviorSubject(value: "")

Then you can have an infinite stream that just gets sent a value when the button is pushed. The actual value sent is not as important as the fact that it emits a value when the user pushes the button.

var buttonPushes = PublishSubject<Bool>()

From that, we're going to create a stream that emits a value each time the button is pushed. We'll represent a login attempt as a struct, LoginInfo that contains all the stuff you need to try and log in.

struct LoginInfo {
    let pin : String
    /* Maybe other stuff like a username is needed here */
}

var loginAttempts = buttonPushes.map { _ in
    LoginInfo(pin: try pin.value())
}

loginAttempts sees a button push and maps in into an attempt to log in.

As part of that mapping, it captures the latest value from the pin stream, but loginAttempts is not directly tied to the pin stream. The pin stream can go on changing forever and loginAttempts won't care until the user pushes the submit button.

Hmm... The code shown only triggers when submit emits a next event, not when pin emits so either you have other code that you aren't showing that is causing the problem, or you are sending a .next event into your publish relay inappropriately.

In short, only send a .next event when the user taps the submit button and the code you posted will work fine. Also, clearing out the pin text field will not change the pin behavior relay unless you are doing something odd elsewhere so that shouldn't be an issue.

This is essentially the same as what you have, but uses the withLatestFrom operator:

class ViewModel {
    let submit = PublishRelay<Void>()
    let pin = BehaviorRelay(value: "")
    let state: Observable<State>

    init(service: LoginService) {
        self.state = submit
            .withLatestFrom(pin)
            .flatMapLatest { [service] in
                service.login(pin: $0)
                    .map { _ in State.active }
                    .catch { _ in .just(.inactive) }
                    .asObservable()
                    .startWith(.loading)
            }
            .startWith(.inactive)
    }
}

I'm not a fan of all the relays though and I don't like that you are throwing away the User object. I would likely do something more like this:

class ViewModel {
    let service: LoginService
    init(service: LoginService) {
        self.service = service
    }

    func state(pin: Observable<String>, submit: Observable<Void>) -> (state: Observable<State>, user: Observable<User?>) {

        let user = submit
            .withLatestFrom(pin)
            .flatMapLatest { [service] in
                service.login(pin: $0)
                    .map(Optional.some)
                    .catchAndReturn(nil)
            }
            .share()

        let state = Observable.merge(
            submit.map { .loading },
            user.map { user in user == nil ? .inactive : .active }
        )
            .startWith(State.inactive)

        return (state: state, user: user)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM