RxSwift:链接流的问题

RxSwift: Issue with chaining streams

我有一个涉及远程服务调用和 pin 的登录用例。

在我的视图模型中,我有一个像这样的 pin 行为继电器

let pin = BehaviorRelay(value: "")

那我有这个服务:

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

我还在视图模型中有一个发布中继(用于支持提交按钮)和一个状态流。状态必须最初设置为 .inactive,一旦提交中继触发,我需要状态变为 .loading 并最终变为 .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)
        })
}

问题是如果 pin 在提交后发生变化(我的用例涉及在单击提交按钮后清除 pin),将使用新的 pin 值(在本例中为空字符串)第二次调用该服务。

我希望此流仅获取 pin 的值和 运行 服务一次,并忽略任何新的 pin 值,除非再次触发提交。

我认为您太过努力将事情串在一起 :-)。
让我们将您的问题分开,看看是否有帮助。

对您来说重要的事件是按下按钮。当用户按下“提交”按钮时,您想尝试登录。

因此,请将主题附加到您的 pin 输入字段,让它捕获用户输入的结果。您希望这是一个包含 pin 最新值的流:

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

然后你可以有一个无限流,当按下按钮时,它只会发送一个值。发送的实际值不如用户按下按钮时发出值重要。

var buttonPushes = PublishSubject<Bool>()

据此,我们将创建一个流,每次按下按钮时都会发出一个值。我们将登录尝试表示为一个结构,LoginInfo 包含您尝试登录所需的所有内容。

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 看到按钮按下并映射到登录尝试。

作为该映射的一部分,它从 pin 流捕获最新值,但 loginAttempts 不直接绑定到 pin 流。 pin 流可以永远改变,并且 loginAttempts 不会关心,直到用户按下提交按钮。

嗯...显示的代码仅在 submit 发出下一个事件时触发,而不是在 pin 发出时触发,所以要么是您有其他未显示的代码导致了问题,或者您不恰当地将 .next 事件发送到您的发布中继。

简而言之,仅在用户点击提交按钮时发送 .next 事件,您发布的代码才能正常工作。此外,清除 pin 文本字段不会改变 pin 行为中继,除非你在其他地方做一些奇怪的事情,所以这不应该成为问题。

这与您所拥有的基本相同,但使用了 withLatestFrom 运算符:

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: [=10=])
                    .map { _ in State.active }
                    .catch { _ in .just(.inactive) }
                    .asObservable()
                    .startWith(.loading)
            }
            .startWith(.inactive)
    }
}

虽然我不是所有中继的粉丝,但我不喜欢您丢弃 User 对象。我可能会做更多这样的事情:

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

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

        let user = submit
            .withLatestFrom(pin)
            .flatMapLatest { [service] in
                service.login(pin: [=11=])
                    .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)
    }
}