Ошибки RxSwift удаляют подписки - PullRequest
0 голосов
/ 04 сентября 2018

Я экспериментировал с некоторыми новыми быстрыми архитектурами и шаблонами и заметил странную проблему с RxSwift, когда кажется, что я выполняю вызов службы и происходит ошибка - например, пользователь вводит неверный пароль - тогда он, похоже, удаляет мои подписки, поэтому я не могу сделать сервисный вызов снова

Я не уверен, почему это происходит. Я сделал небольшой мини-проект, демонстрирующий проблему с примером приложения для входа.

Моя ViewModel выглядит так

import RxSwift
import RxCocoa
import RxCoordinator
import RxOptional
extension LoginModel : ViewModelType {
    struct Input {
        let loginTap : Observable<Void>
        let password : Observable<String>
    }

    struct Output {
        let validationPassed : Driver<Bool>
        let loginActivity : Driver<Bool>
        let loginServiceError : Driver<Error>
        let loginTransitionState : Observable<TransitionObservables>
    }

    func transform(input: LoginModel.Input) -> LoginModel.Output {
        // check if email passes regex
        let isValid = input.password.map{(val) -> Bool in
            UtilityMethods.isValidPassword(password: val)
        }

        // handle response
        let loginResponse = input.loginTap.withLatestFrom(input.password).flatMapLatest { password in
            return self.service.login(email: self.email, password: password)
        }.share()

        // handle loading
        let loginServiceStarted = input.loginTap.map{true}
        let loginServiceStopped = loginResponse.map{_ in false}
        let resendActivity = Observable.merge(loginServiceStarted, loginServiceStopped).materialize().map{$0.element}.filterNil()

        // handle any errors from service call
        let serviceError = loginResponse.materialize().map{$0.error}.asDriver(onErrorJustReturn: RxError.unknown).filterNil()

        let loginState = loginResponse.map { _ in
            return self.coordinator.transition(to: .verifyEmailController(email : self.email))
        }

        return Output(validationPassed : isValid.asDriver(onErrorJustReturn: false), loginActivity: resendActivity.asDriver(onErrorJustReturn: false), loginServiceError: serviceError, loginTransitionState : loginState)
    }
}

class LoginModel {
    private let coordinator: AnyCoordinator<WalkthroughRoute>
    let service : LoginService
    let email : String
    init(coordinator : AnyCoordinator<WalkthroughRoute>, service : LoginService, email : String) {
        self.service = service
        self.email = email
        self.coordinator = coordinator
    } 
}

А мой ViewController выглядит так

import UIKit
import RxSwift
import RxCocoa
class TestController: UIViewController, WalkthroughModuleController, ViewType {

    // password
    @IBOutlet var passwordField : UITextField!

    // login button
    @IBOutlet var loginButton : UIButton!

    // disposes of observables
    let disposeBag = DisposeBag()

    // view model to be injected
    var viewModel : LoginModel!

    // loader shown when request is being made
    var generalLoader : GeneralLoaderView?

    override func viewDidLoad() {
        super.viewDidLoad()

    }
    // bindViewModel is called from route class
    func bindViewModel() {
        let input = LoginModel.Input(loginTap: loginButton.rx.tap.asObservable(), password: passwordField.rx.text.orEmpty.asObservable())

        // transforms input into output
        let output = transform(input: input)

        // fetch activity
        let activity = output.loginActivity

        // enable/disable button based on validation
        output.validationPassed.drive(loginButton.rx.isEnabled).disposed(by: disposeBag)

        // on load
        activity.filter{$0}.drive(onNext: { [weak self] _ in
            guard let strongSelf = self else { return }
            strongSelf.generalLoader = UtilityMethods.showGeneralLoader(container: strongSelf.view, message: .Loading)
        }).disposed(by: disposeBag)

        // on finish loading
        activity.filter{!$0}.drive(onNext : { [weak self] _ in
            guard let strongSelf = self else { return }
            UtilityMethods.removeGeneralLoader(generalLoader: strongSelf.generalLoader)
        }).disposed(by: disposeBag)

        // if any error occurs
        output.loginServiceError.drive(onNext: { [weak self] errors in
            guard let strongSelf = self else { return }

            UtilityMethods.removeGeneralLoader(generalLoader: strongSelf.generalLoader)

            print(errors)
        }).disposed(by: disposeBag)

        // login successful
        output.loginTransitionState.subscribe().disposed(by: disposeBag)
    }
}

Мой класс обслуживания

import RxSwift
import RxCocoa

struct LoginResponseData : Decodable {
    let msg : String?
    let code : NSInteger
}

    class LoginService: NSObject {
        func login(email : String, password : String) -> Observable<LoginResponseData> {
            let url = RequestURLs.loginURL

            let params = ["email" : email,
                          "password": password]

            print(params)

            let request = AFManager.sharedInstance.setupPostDataRequest(url: url, parameters: params)
            return request.map{ data in
                return try JSONDecoder().decode(LoginResponseData.self, from: data)
            }.map{$0}
        }
    }

Если я введу действительный пароль, запрос будет работать нормально. Если я удаляю код перехода для целей тестирования, я мог бы продолжать вызывать службу входа снова и снова, пока пароль действителен. Но как только возникает какая-либо ошибка, наблюдаемые объекты, относящиеся к служебному вызову, удаляются, поэтому пользователь больше не может пытаться повторить служебный вызов

Пока что я нашел единственный способ исправить это, если возникнет какая-либо ошибка, снова вызвать bindViewModel, чтобы снова настроить подписку. Но это похоже на очень плохую практику.

Любой совет будет высоко ценится!

Ответы [ 2 ]

0 голосов
/ 05 сентября 2018

В том месте, где вы совершаете вход, звоните:

let loginResponse = input.loginTap.withLatestFrom(input.password).flatMapLatest { password in
    return self.service.login(email: self.email, password: password)
}.share()

Вы можете сделать одну из двух вещей. Сопоставьте логин с типом Result<T>.

let loginResponse = input.loginTap.withLatestFrom(input.password).flatMapLatest { password in
    return self.service.login(email: self.email, password: password)
        .map(Result<LoginResponse>.success)
        .catchError { Observable.just(Result<LoginResponse>.failure($0)) }
    }.share()

Или вы можете использовать оператор материализации.

let loginResponse = input.loginTap.withLatestFrom(input.password).flatMapLatest { password in
    return self.service.login(email: self.email, password: password)
        .materialize()
    }.share()

Любой метод изменяет тип вашего loginResponse объекта, заключая его в перечисление (либо Result<T>, либо Event<T>. В этом случае вы можете обрабатывать ошибки иначе, чем допустимые результаты, не нарушая цепочку Observable. и без потери ошибки.

Другой вариант, как вы обнаружили, заключается в изменении типа loginResponse на необязательный, но затем вы теряете объект ошибки.

0 голосов
/ 04 сентября 2018

Поведение не странное, но работает как положено: Как указано в официальной документации RxSwift Документация : «Когда последовательность отправляет завершенное событие или событие ошибки, все внутренние ресурсы, которые вычисляют элементы последовательности, будут освобождены». Для вашего примера это означает, что неудачная попытка входа в систему приведет к тому, что метод func login(email : String, password : String) -> Observable<LoginResponseData> вернет ошибку, то есть вернет Observable<error>, которая будет:

  • с одной стороны перенести эту ошибку всем своим подписчикам (что и сделает ваш ВК)
  • с другой стороны, располагаем наблюдаемым

Чтобы ответить на ваш вопрос, что вы можете сделать, кроме повторной подписки, чтобы сохранить подписку: вы можете просто использовать .catchError(), чтобы наблюдаемое не прекращалось, и вы можете сами решить, что вы хотите вернуть после возникновения ошибки. Обратите внимание, что вы также можете проверить ошибку для определенного домена ошибок и возвращать ошибки только для определенных доменов.

Я лично вижу ответственность за обработку ошибок в руках соответствующих подписчиков, т.е. в вашем случае ваш TestController (чтобы вы могли использовать .catchError() там), но если вы хотите быть уверенным, что наблюдаемое возвращается из с func login(email : String, password : String) -> Observable<LoginResponseData> даже не пересылаются сообщения об ошибках для всех подписок, вы также можете использовать .catchError() здесь, хотя я вижу проблемы с возможным неправильным поведением.

...