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)
}
}
我有一个涉及远程服务调用和 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)
}
}