Error Handling - 错误处理
一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:
- retry - 重试
- catch - 恢复
retry - 重试
retry 可以让序列在发生错误后重试:
1. // 请求 JSON 失败时,立即重试,
2. // 重试 3 次后仍然失败,就将错误抛出
4. let rxJson: Observable<JSON> = ...
6. rxJson
7. .retry(3)
8. .subscribe(onNext: { json in
9. print("取得 JSON 成功: \(json)")
10. }, onError: { error in
11. print("取得 JSON 失败: \(error)")
12. })
13. .disposed(by: disposeBag)
以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。
retryWhen
如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:
1. // 请求 JSON 失败时,等待 5 秒后重试,
3. let retryDelay: Double = 5 // 重试延时 5 秒
5. rxJson
6. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
7. return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
8. }
9. .subscribe(...)
10. .disposed(by: disposeBag)
这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:
1. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
2. ...
3. }
闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。
如果需要加上一个最大重试次数的限制:
1. // 请求 JSON 失败时,等待 5 秒后重试,
2. // 重试 4 次后仍然失败,就将错误抛出
4. let maxRetryCount = 4 // 最多重试 4 次
5. let retryDelay: Double = 5 // 重试延时 5 秒
7. rxJson
8. .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
9. return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
10. guard index < maxRetryCount else {
11. return Observable.error(error)
12. }
13. return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
14. }
15. }
16. .subscribe(...)
17. .disposed(by: disposeBag)
我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:
1. ...
2. rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
3. guard index < maxRetryCount else {
4. return Observable.error(error)
5. }
6. return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
7. }
8. ...
我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。
catchError - 恢复
catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:
1. searchBar.rx.text.orEmpty
2. ...
3. .flatMapLatest { query -> Observable<[Repository]> in
4. ...
5. return searchGitHub(query)
6. .catchErrorJustReturn([])
7. }
8. ...
9. .bind(to: ...)
10. .disposed(by: disposeBag)
我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。
你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:
1. // 先从网络获取数据,如果获取失败了,就从本地缓存获取数据
3. let rxData: Observable<Data> = ... // 网络请求的数据
4. let cahcedData: Observable<Data> = ... // 之前本地缓存的数据
6. rxData
7. .catchError { _ in cahcedData }
8. .subscribe(onNext: { date in
9. print("获取数据成功: \(date.count)")
10. })
11. .disposed(by: disposeBag)
Result
如果我们只是想给用户错误提示,那要如何操作呢?
以下提供一个最为直接的方案,不过这个方案存在一些问题:
1. // 当用户点击更新按钮时,
2. // 就立即取出修改后的用户信息。
3. // 然后发起网络请求,进行更新操作,
4. // 一旦操作失败就提示用户失败原因
6. updateUserInfoButton.rx.tap
7. .withLatestFrom(rxUserInfo)
8. .flatMapLatest { userInfo -> Observable<Void> in
9. return update(userInfo)
10. }
11. .observeOn(MainScheduler.instance)
12. .subscribe(onNext: {
13. print("用户信息更新成功")
14. }, onError: { error in
15. print("用户信息更新失败: \(error.localizedDescription)")
16. })
17. .disposed(by: disposeBag)
这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。
为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如使用枚举 Result:
1. // 自定义一个枚举类型 Result
2. public enum Result<T> {
3. case success(T)
4. case failure(Swift.Error)
5. }
然后之前的代码需要修改成:
1. updateUserInfoButton.rx.tap
2. .withLatestFrom(rxUserInfo)
3. .flatMapLatest { userInfo -> Observable<Result<Void>> in
4. return update(userInfo)
5. .map(Result.success) // 转换成 Result
6. .catchError { error in Observable.just(Result.failure(error)) }
7. }
8. .observeOn(MainScheduler.instance)
9. .subscribe(onNext: { result in
10. switch result { // 处理 Result
11. case .success:
12. print("用户信息更新成功")
13. case .failure(let error):
14. print("用户信息更新失败: \(error.localizedDescription)")
15. }
16. })
17. .disposed(by: disposeBag)
这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。就算网络请求失败,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。
另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!
