RxSwift错误处理了订阅

时间:2018-09-04 10:55:55

标签: swift rx-swift

我一直在尝试一些新的swift架构和模式,但我发现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 :(得分:1)

行为并不奇怪,但是可以按预期工作:如官方RxSwift文档documentation中所述: “当序列发送完成事件或错误事件时,将释放所有计算序列元素的内部资源。” 对于您的示例,这意味着登录尝试失败将导致方法func login(email : String, password : String) -> Observable<LoginResponseData>返回错误,即返回Observable<error>,该错误将:

  • 一方面将这个错误快速转发给所有订阅者(这将由您的VC完成)
  • 另一方面,
  • 放置可观察物

要回答您的问题,除了重新订阅之外,您还可以做什么以维持订阅:您可以使用.catchError(),因此可观察对象不会终止,您可以自己决定要什么错误发生后返回。请注意,您还可以检查特定错误域的错误,并仅返回特定域的错误。

我个人看到错误处理的责任在各个订阅者的手中,也就是说,在您的情况下,您的TestController(因此您可以在此处使用.catchError()),但是如果要确保从func login(email : String, password : String) -> Observable<LoginResponseData>返回的可观察值甚至无法快速转发所有订阅的任何错误,尽管我会看到潜在的不当行为问题,但您也可以在此处使用.catchError()

答案 1 :(得分:1)

在您进行登录呼叫的地方:

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的类型更改为可选类型,但随后您将松开错误对象。