Error Handling - 错误处理

https://beeth0ven.github.io/RxSwift-Chinese-Documentation/content/rxswift_core/error_handling.html

一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

  • retry - 重试
  • catch - 恢复

retry

retry 可以让序列在发生错误后重试:

// 请求失败时,立即重试,
// 重试 3 次后仍然失败,就将错误抛出
func testRetry() {
    getDictObservable()
        .retry(3)   // 总共调用3次包括第一次
        .subscribe({ (e) in
            print("in the end: \(e.debugDescription)")
        })
        .disposed(by: disposeBag)
}

以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

retryWhen

如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

func testRetryWhen() {
    let retryDelay: RxTimeInterval = 1.0
    getDictObservable()
        .retryWhen({ (rxError) -> Observable<Int> in
            return rxError.flatMap({ (e) -> Observable<Int> in
                return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
            })
        })
        .subscribe({ (e) in
            print("in the end: \(e.debugDescription)")
        })
        .disposed(by: disposeBag)
}

这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
    ...
}

闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

如果需要加上一个最大重试次数的限制:

func testRetryWhenAndMaxRetry() {
    let retryDelay: RxTimeInterval = 1.0
    let maxRetryCount: Int = 4
    getDictObservable()
        .retryWhen({ (rxError) -> Observable<Int> in
            return rxError.enumerated().flatMap({ (index, element) -> Observable<Int> in
                // 最多 4 次
                if index >= maxRetryCount {
                    let err = TError.init(errorCode: 0, errorString: "Retry Too Many Times", errorData: nil)
                    return Observable.error(err)
                }
                return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
            })
        })
        .subscribe({ (e) in
            print("in the end: \(e.debugDescription)")
        })
        .disposed(by: disposeBag)
}

我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 1 秒后重试:

...
rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
    guard index < maxRetryCount else {
        return Observable.error(error)
    }
    return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
...

我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。

catchErrorJustReturn - 恢复

catchErrorJustReturn 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

func testCatchErrorJustReturn() {
    let defaultDict = ["returnDefaultDict": "Is DefaultDict"]
    getDictObservable()
        .catchErrorJustReturn(defaultDict)
        .subscribe ({ (e) in
            print(e)
        })
        .disposed(by: disposeBag)
}

catchErrorJustReturn

当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

func testCatchError() {
    let defaultDictVariable: Variable<[AnyHashable: Any]> = Variable.init(["returnDefaultDict": "Is DefaultDictVariable"])
    getDictObservable()
        .catchError({ (error) -> Observable<[AnyHashable : Any]> in
            return defaultDictVariable.asObservable()
        })
        .subscribe ({ (e) in
            print(e)
        })
        .disposed(by: disposeBag)
}

Result

如果我们只是想给用户错误提示,那要如何操作呢?

以下提供一个最为直接的方案,不过这个方案存在一些问题:

// 当用户点击更新按钮时,
// 就立即取出修改后的用户信息。
// 然后发起网络请求,进行更新操作,
// 一旦操作失败就提示用户失败原因

updateUserInfoButton.rx.tap
    .withLatestFrom(rxUserInfo)
    .flatMapLatest { userInfo -> Observable<Void> in
        return update(userInfo)
    }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: {
        print("用户信息更新成功")
    }, onError: { error in
        print("用户信息更新失败: \(error.localizedDescription)")
    })
    .disposed(by: disposeBag)

这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如使用枚举 Result

// 自定义一个枚举类型 Result
public enum Result<T> {
    case success(T)
    case failure(Swift.Error)
}

然后之前的代码需要修改成:

updateUserInfoButton.rx.tap
    .withLatestFrom(rxUserInfo)
    .flatMapLatest { userInfo -> Observable<Result<Void>> in
        return update(userInfo)
            .map(Result.success)  // 转换成 Result
            .catchError { error in Observable.just(Result.failure(error)) }
    }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { result in
        switch result {           // 处理 Result
        case .success:
            print("用户信息更新成功")
        case .failure(let error):
            print("用户信息更新失败: \(error.localizedDescription)")
        }
    })
    .disposed(by: disposeBag)

这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。就算网络请求失败,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

示例:


// MARK: ═══════════════════════════════════════
// MARK:           Test ResultModel        
// MARK: ═══════════════════════════════════════
@IBOutlet weak var testInPreviousWayButton: UIButton!
@IBOutlet weak var testResultModelButton: UIButton!
override func viewDidLoad() {
    super.viewDidLoad()
    testInPreviousWay()
    testResultModel()
}

// testInPreviousWay
func testInPreviousWay() {
    // 如果这部分代码只会运行一次(只进行一次绑定)
    // 此时如果发生error事件之后则被丢弃,后续点击则无法再响应
    testInPreviousWayButton.rx.tap
        .flatMapLatest({ [unowned self] (_) -> Observable<[AnyHashable: Any]> in
            return self.getDictObservable()
        })
        .subscribe(onNext: { [unowned self] (dict) in
            print("Button is Taped: \(self.testInPreviousWayButton.titleLabel?.text ?? "" ) ")
            print("get dict success: \(dict)")
        }, onError: { (_) in
            // 此处一旦进入,订阅将失效,后续点击不会响应
            print("Button is Taped: \(self.testInPreviousWayButton.titleLabel?.text ?? "" ) ")
            // err.printLog()
        })
        .disposed(by: disposeBag)
}

// testResultModel
func testResultModel() {
    enum ResultModel<T> {
        case success(T)
        case failure(Error)
    }

    // 如果这部分代码只会运行一次(只进行一次绑定)
    // 此时如果发生error事件之后则会被转成ResultMode.failure(err)
    // 此时拦截了error事件,订阅不会丢弃,后续点击可以继续响应
    testResultModelButton.rx.tap
        .flatMapLatest({ [unowned self] (_) -> Observable<ResultModel<[AnyHashable: Any]>> in
            return self.getDictObservable()
                .map(ResultModel<[AnyHashable: Any]>.success)
                .catchError({ (error) -> Observable<ResultModel<[AnyHashable : Any]>> in
                    return Observable.just(ResultModel.failure(error))
                })
        })
        .subscribe(onNext: { [unowned self] (resultModel) in
            switch resultModel {
            case .success(let dict):
                print("Is In ResultModel & Button is Taped: \(self.testResultModelButton.titleLabel?.text ?? "" ) ")
                print("get dict success: \(dict)")
            case .failure(_):
                print("Is In ResultModel & Button is Taped: \(self.testResultModelButton.titleLabel?.text ?? "" ) ")
                // err.printLog()
            }
        }, onError: { [unowned self] (err) in
            // 此处永远不会进入
            print("Is In Subscribe Error & Button is Taped: \(self.testResultModelButton.titleLabel?.text ?? "" ) ")
            err.printLog()
        })
        .disposed(by: disposeBag)
}

另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift

results matching ""

    No results matching ""