RxSwift: Проблема с цепочками потоков

#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 ему будет все равно, пока пользователь не нажмет кнопку «Отправить».