很明显,我是RxSwift的新手,虽然我花了很多文档和演讲,但我认为我缺少一些基本概念。
在我的应用程序中,我有一个RESTful web服务来加载各种资源,但web服务的基本url在构建/启动时是未知的。相反,我有一个"URL解析器"web服务,我可以用我的应用程序捆绑包、版本和可能的环境("生产"、"调试"或在应用程序调试设置中输入的任何自定义字符串)来调用它,以获得我随后用于实际服务的基本URL。
我的想法是创建两个服务,一个用于URL解析程序,另一个用于实际的web服务,这为我提供了资源。URL解析程序将有一个Variable和一个Observable。我使用该变量来表示需要通过对url解析器的web服务调用来刷新基本url。我通过观察变量来做到这一点,并只过滤真值。服务类中的一个函数将变量值设置为true(最初为false),在过滤变量的观察器中,我在另一个Observable中调用web服务(本例使用伪JSON web服务):
import Foundation
import RxSwift
import Alamofire
struct BaseURL: Codable {
let title: String
}
struct URLService {
private static var counter = 0
private static let urlVariable: Variable<Bool> = Variable(false)
static let urlObservable: Observable<BaseURL> = urlVariable.asObservable()
.filter { counter += 1; return $0 }
.flatMap { _ in
return Observable.create { observer in
let url = counter < 5 ? "https://jsonplaceholder.typicode.com/posts" : ""
let requestReference = Alamofire.request(url).responseJSON { response in
do {
let items = try JSONDecoder().decode([BaseURL].self, from: response.data!)
observer.onNext(items[0])
} catch {
observer.onError(error)
}
}
return Disposables.create() {
requestReference.cancel()
}
}
}
static func getBaseUrl() {
urlVariable.value = true;
}
static func reset() {
counter = 0;
}
}
现在的问题是,有时web服务调用可能会失败,我需要向用户显示错误,以便重试。我认为onError对此很有用,但它似乎永远会杀死所有订阅者。
我可以把订阅放在它自己的函数中,在Observer的错误处理程序中,我可以显示一个警报,然后再次调用订阅函数,如下所示:
func subscribe() {
URLService.urlObservable.subscribe(onNext: { (baseURL) in
let alert = UIAlertController(title: "Success in Web Service", message: "Base URL is (baseURL.title)", preferredStyle: .alert)
let actionYes = UIAlertAction(title: "Try again!", style: .default, handler: { action in
URLService.getBaseUrl()
})
alert.addAction(actionYes)
DispatchQueue.main.async {
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
}
}, onError: { error in
let alert = UIAlertController(title: "Error in Web Service", message: "Something went wrong: (error.localizedDescription)", preferredStyle: .alert)
let actionYes = UIAlertAction(title: "Yes", style: .default, handler: { action in
URLService.reset()
self.subscribe()
})
alert.addAction(actionYes)
DispatchQueue.main.async {
VesselService.reset()
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
}
}).disposed(by: disposeBag)
}
然后在我的AppDelegate中,我会调用
subscribe()
URLService.getBaseUrl()
问题是,所有其他观察者也会因错误而死亡,但由于URLService.urlObservable上唯一的其他观察者是我的另一个web服务类,我想我也可以在那里实现相同风格的订阅函数。
我读到有人建议返回一个Result枚举,它有两种情况:实际结果(.success(Result:T))或错误(.error(error:error))。
那么,在Rx中处理错误web服务错误的更好方法是什么呢?我无法理解这个问题,我花了两天时间试图理解它。有什么想法或建议吗?
更新
我突然想到,我可以完全忽略web服务调用中的错误,而是将任何错误发布到全局"错误"变量中,我的应用程序代表可以观察到该变量以显示警报。"错误"可能引用了最初导致错误的函数,因此可以重试。我仍然很困惑,不知道该怎么办
更新2
我想我可能会找到一个可行的解决方案。由于我还是Rx和RxSwift的初学者,我很乐意接受改进建议。在编写实际代码时,我将调用链分为两部分:
- 我进行web服务调用的部分
- 我点击按钮并处理web服务结果的部分,无论是错误还是成功
在我单击按钮并处理结果的部分,我使用catchError并按照注释中的建议重试。代码如下:
let userObservable = URLService
.getBaseUrl(environment: UserDefaults.standard.environment) //Get base url from web service 1
.flatMap({ [unowned self] baseURL -> Observable<User> in
UserService.getUser(baseURL: baseURL,
email: self.usernameTextField.text!,
password: self.passwordTextField.text!) //Get user from web service 2 using the base url from webservice 1
})
signInButton
.rx
.tap
.throttle(0.5, scheduler: MainScheduler.instance)
.flatMap({ [unowned self] () -> Observable<()> in
Observable.create { observable in
let hud = MBProgressHUD.present(withTitle: "Signing in...");
self.hud = hud
observable.onNext(())
return Disposables.create {
hud?.dismiss()
}
}
})
.flatMap({ () -> Observable<User> in
return userObservable
})
.catchError({ [unowned self] error -> Observable<User> in
self.hud?.dismiss()
self.handleError(error)
return userObservable
})
.retry()
.subscribe(onNext: { [unowned self] (user) in
UserDefaults.standard.accessToken = user.accessToken
UserDefaults.standard.tokenType = user.tokenType
self.hud?.dismiss()
})
.disposed(by: disposeBag)
诀窍是将对两个web服务的调用从cain移到它们自己的变量中,这样我就可以随时重新调用它。当我现在返回"userObservable",并且在web服务调用期间发生错误时,我可以在catchError中显示错误,并在下次重试时返回相同的"userObserver"。
目前,只有当web服务调用链中出现错误时,它才能正确处理错误,所以我认为我应该让按钮点击驱动程序。
好吧,对于每一个来到这里的人来说,你可能对Rx世界应该如何运作缺乏理解或误解。我有时仍然觉得很困惑,但我找到了一个比我在最初的问题中发布的更好的解决方案。
在Rx中,一个错误"杀死"或更确切地说,完成了链中的所有观察者,这实际上是一件好事。如果在web服务调用中存在预期的错误,如API错误,您应该尝试在它们发生的地方处理它们,或者将它们视为预期值。
例如,观察者可以返回一个可选类型,订阅者可以过滤值的存在。如果API调用中发生错误,则返回nil。其他"错误处理程序"可以筛选nil值,以向用户显示错误消息。
同样可行的是返回带有两种情况的Result枚举:.success(值:T)和.error(错误:error)。您将错误视为可接受的结果,观察者负责检查它是否应该显示错误消息或成功结果值。
还有另一种选择,这当然不是最好的,但它可以简单地将您预计会失败的呼叫嵌套在呼叫的订户中,而该订户一定不会受到影响。在我的情况下,这是一个按钮点击,导致调用网络服务。
我原来帖子的"更新2"将变成:
signInButton.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] () in
log.debug("Trying to sign user in. Presenting HUD")
self.hud = MBProgressHUD.present(withTitle: "Signing in...");
self.viewModel.signIn()
.subscribe(onNext: { [unowned self] user in
log.debug("User signed in successfully. Dismissing HUD")
self.hud?.dismiss()
}, onError: { [unowned self] error in
log.error("Failed to sign user in. Dismissing HUD and presenting error: (error)")
self.hud?.dismiss()
self.handleError(error)
}).disposed(by: self.disposeBag)
}).disposed(by: self.disposeBag)
MVVM视图模型对web服务进行如下调用:
func signIn() -> Observable<User> {
log.debug("HUD presented. Loading BaseURL to sign in User")
return URLService.getBaseUrl(environment: UserDefaults.standard.environment)
.flatMap { [unowned self] baseURL -> Observable<BaseURL> in
log.debug("BaseURL loaded. Checking if special env is used.")
if let specialEnv = baseURL.users[self.username.value] {
log.debug("Special env is used. Reloading BaseURL")
UserDefaults.standard.environment = specialEnv
return URLService.getBaseUrl(environment: specialEnv)
} else {
log.debug("Current env is used. Returning BaseURL")
return Observable.just(baseURL)
}
}
.flatMap { [unowned self] baseURL -> Observable<User> in
log.debug("BaseURL to use is: (baseURL.url). Now signing in User.")
let getUser = UserService.getUser(baseURL: baseURL.url, email: self.username.value, password: self.password.value).share()
getUser.subscribe(onError: { error in
UserDefaults.standard.environment = nil
}).disposed(by: self.disposeBag)
return getUser
}
.map{ user in
UserDefaults.standard.accessToken = user.accessToken
UserDefaults.standard.tokenType = user.tokenType
return user
}
}
首先,我想在按下按钮时只调用视图模型signIn()函数,但由于视图模型中不应该有UI代码,我认为显示和取消HUD是ViewController的责任。
我认为这个设计现在相当坚固。按钮观察器永远不会完成,并且可以永远继续发送事件。早些时候,如果出现第二个错误,按钮观察器可能会死亡,而我的日志显示userObservable被执行了两次,这也一定不会发生。
我只是想知道是否有比嵌套订阅者更好的方法。