RxSwift:链接流的问题

Posted

技术标签:

【中文标题】RxSwift:链接流的问题【英文标题】:RxSwift: Issue with chaining streams 【发布时间】:2021-10-23 15:42:06 【问题描述】:

我有一个涉及远程服务调用和 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 的任何新值,除非再次触发提交。

【问题讨论】:

【参考方案1】:

嗯...显示的代码仅在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: $0)
                    .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: $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)
    

【讨论】:

【参考方案2】:

我认为您过于努力地将事物链接在一起:-)。 让我们分解您的问题,看看是否有帮助。

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

因此,将主题附加到您的 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 不会在意,直到用户按下提交按钮。

【讨论】:

以上是关于RxSwift:链接流的问题的主要内容,如果未能解决你的问题,请参考以下文章

Swift (RxSwift):使用泛型链接 ViewItem 和 Cell 类

使用 RxSwift 链接登录操作

RxSwift: 链 Completable 到 Observable

RxSwift + 用户默认值

RxSwift 映射问题

如何正确安装“RxSwift”模块?