#swift #rx-swift
Вопрос:
У меня есть вариант использования входа в систему, который включает удаленный вызов службы и 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)
}
}
Однако я не поклонник всех ретрансляторов, и мне не нравится, что вы выбрасываете объект пользователя. Я бы, скорее всего, сделал что-то более похожее на это:
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
ему будет все равно, пока пользователь не нажмет кнопку «Отправить».