RxSwift 错误处置订阅
RxSwift errors dispose of subscriptions
我一直在试验一些新的 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{[=10=].element}.filterNil()
// handle any errors from service call
let serviceError = loginResponse.materialize().map{[=10=].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{[=11=]}.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{![=11=]}.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)
}
}
我的服务class
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{[=12=]}
}
}
如果我输入有效密码,请求就可以正常工作。如果出于测试目的删除转换代码,只要密码有效,我就可以一遍又一遍地调用登录服务。但是一旦发生任何错误,与服务调用相关的可观察对象就会被处理掉,这样用户就不能再尝试服务调用了
到目前为止,我发现解决此问题的唯一方法是,如果发生任何错误,请再次调用 bindViewModel 以便重新设置订阅。但这似乎是非常糟糕的做法。
如有任何建议,我们将不胜感激!
该行为并不奇怪,但按预期工作:如官方 RxSwift 文档所述documentation:
"When a sequence sends the completed or error event all internal resources that compute sequence elements will be freed."
对于您的示例,这意味着登录尝试失败将导致方法 func login(email : String, password : String) -> Observable<LoginResponseData>
到 return 出错,即 return Observable<error>
,这将:
- 一方面,将此错误快速转发给所有订阅者(这将由您完成 VC)
- 另一方面处理可观察对象
回答你的问题,除了再次订阅你还能做什么,以维持订阅:你可以只使用 .catchError()
,所以 observable 不会终止,你可以自己决定什么你想在发生错误后return。请注意,您还可以检查特定错误域的错误和仅特定域的 return 错误。
我个人认为错误处理的责任在各自的订阅者手中,即在你的情况下你的 TestController
(所以你可以在那里使用 .catchError()
),但如果你想确保来自 func login(email : String, password : String) -> Observable<LoginResponseData>
的可观察 returned 甚至不会快进所有订阅的任何错误,您也可以在此处使用 .catchError()
,尽管我会看到潜在的不当行为问题。
在您调用登录的地方:
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
}
.share()
您可以做以下两件事之一。将登录映射到 Result<T>
类型。
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.map(Result<LoginResponse>.success)
.catchError { Observable.just(Result<LoginResponse>.failure([=11=])) }
}
.share()
或者您可以使用实体化运算符。
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.materialize()
}
.share()
这两种方法都通过将对象包装在枚举(Result<T>
或 Event<T>
中来更改 loginResponse
对象的类型。然后,您可以采用与以往不同的方式处理错误在不破坏 Observable 链和不丢失错误的情况下获得合法结果。
如您所见,另一种选择是将 loginResponse
的类型更改为可选类型,但随后您丢失了错误对象。
我一直在试验一些新的 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{[=10=].element}.filterNil()
// handle any errors from service call
let serviceError = loginResponse.materialize().map{[=10=].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{[=11=]}.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{![=11=]}.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)
}
}
我的服务class
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{[=12=]}
}
}
如果我输入有效密码,请求就可以正常工作。如果出于测试目的删除转换代码,只要密码有效,我就可以一遍又一遍地调用登录服务。但是一旦发生任何错误,与服务调用相关的可观察对象就会被处理掉,这样用户就不能再尝试服务调用了
到目前为止,我发现解决此问题的唯一方法是,如果发生任何错误,请再次调用 bindViewModel 以便重新设置订阅。但这似乎是非常糟糕的做法。
如有任何建议,我们将不胜感激!
该行为并不奇怪,但按预期工作:如官方 RxSwift 文档所述documentation:
"When a sequence sends the completed or error event all internal resources that compute sequence elements will be freed."
对于您的示例,这意味着登录尝试失败将导致方法 func login(email : String, password : String) -> Observable<LoginResponseData>
到 return 出错,即 return Observable<error>
,这将:
- 一方面,将此错误快速转发给所有订阅者(这将由您完成 VC)
- 另一方面处理可观察对象
回答你的问题,除了再次订阅你还能做什么,以维持订阅:你可以只使用 .catchError()
,所以 observable 不会终止,你可以自己决定什么你想在发生错误后return。请注意,您还可以检查特定错误域的错误和仅特定域的 return 错误。
我个人认为错误处理的责任在各自的订阅者手中,即在你的情况下你的 TestController
(所以你可以在那里使用 .catchError()
),但如果你想确保来自 func login(email : String, password : String) -> Observable<LoginResponseData>
的可观察 returned 甚至不会快进所有订阅的任何错误,您也可以在此处使用 .catchError()
,尽管我会看到潜在的不当行为问题。
在您调用登录的地方:
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
}
.share()
您可以做以下两件事之一。将登录映射到 Result<T>
类型。
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.map(Result<LoginResponse>.success)
.catchError { Observable.just(Result<LoginResponse>.failure([=11=])) }
}
.share()
或者您可以使用实体化运算符。
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.materialize()
}
.share()
这两种方法都通过将对象包装在枚举(Result<T>
或 Event<T>
中来更改 loginResponse
对象的类型。然后,您可以采用与以往不同的方式处理错误在不破坏 Observable 链和不丢失错误的情况下获得合法结果。
如您所见,另一种选择是将 loginResponse
的类型更改为可选类型,但随后您丢失了错误对象。