虽然 RxSwift 提供了丰富的处理错误操作符,如 retry retryWhen catchError ,但做一些定制化的处理机制直接使用这几个操作符可能是不够的,本文介绍了如何定制一些更友好的处理方案,比如,在网络错误时,由用户决定是否需要重新请求。

阅读本文前建议先阅读 RxSwift - 为什么存在 catchError

本节内容灵感来自:https://github.com/ReactiveX/RxSwift/blob/4b0c77246330ec45fe03beef9ea4f5624085d8a0/RxExample/RxExample/Services/ReachabilityService.swift#L78-L92

retryWhen

直接使用 retry 通常不是我们想要的,我们可不想在手机没有网络情况下,一直尝试网络请求是不够友好的。

针对上述情况,我们可能采取下面两种方案解决:

  • 结束本次操作,展示错误信息;
  • 结束本次操作,展示错误信息,并给出弹窗由用户决定是否重试。

这里我们来用 retryWhen 完成第二种方案。

为了更好的展示 retry 的效果,本次 demo 引入了框架 [SwiftRandom][https://github.com/thellimist/SwiftRandom]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// 自定义的错误
///
/// - notPositive: 不是正数
/// - oversize: 数字过大
enum MyError: Swift.Error {
case notPositive(value: Int)
case oversize(value: Int)
}
Observable<Int>
.deferred { () -> Observable<Int> in
return Observable.just(Int.random(within: -100...200))
}
.map { value -> Int in
if value <= 0 {
throw MyError.notPositive(value: value)
} else if value > 100 {
throw MyError.oversize(value: value)
} else {
return value
}
}

上述代码中我们使用 deferred 确保每次订阅都是一个随机值。

当小于 0 时,抛出一个小于 0 的错误;当大于 100 时,抛出一个数值过大的错误。

在后面接入我们的 retryWhen 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.retryWhen { [unowned self] (errorObservable: Observable<MyError>) -> Observable<()> in
errorObservable
.flatMap { error -> Observable<()> in
switch error {
case let .notPositive(value):
return showAlert(title: "遇到了一个错误,是否重试?", message: "错误信息\(value) 小于 0", for: self)
.map { isEnsure in
if isEnsure {
return ()
} else {
throw error
}
}
case .oversize:
return Observable.error(error)
}
}
}

这次采取的方案是:

  • 当遇到小于 0 时,弹出弹窗,由用户决定是否进行重试;
  • 当大于 100 时,不进行重试。

你可以看到我在这里指明了具体类型 (errorObservable: Observable<MyError>) ,如果遇到的是其他 Error 类型,我们将直接忽略掉。

为了获取所有的 Error ,你可以使用 Observable<Swift.Error>

catchError

使用 catchError 也能完成类似的需求。

上层事件传递和上一小结完全一样,为了处理通用 Error ,我添加了 LocalizedError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// 自定义的错误
///
/// - notPositive: 不是正数
/// - oversize: 数字过大
enum MyError: Swift.Error, LocalizedError {
case notPositive(value: Int)
case oversize(value: Int)
var errorDescription: String? {
switch self {
case let .notPositive(value):
return "\(value)不是正数"
case let .oversize(value):
return "\(value)过大"
}
}
}
Observable<Int>
.deferred { () -> Observable<Int> in
return Observable.just(Int.random(within: -100...200))
}
.map { value -> Int in
if value <= 0 {
throw MyError.notPositive(value: value)
} else if value > 100 {
throw MyError.oversize(value: value)
} else {
return value
}
}

我们在后面接上 catchErrorretry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.catchError { (error) -> Observable<Int> in
return Observable.create { [unowned self] observer in
let alert = UIAlertController(title: "遇到了一个错误,重试还是使用默认值 1 替换?", message: "错误信息:\(error.localizedDescription)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "重试", style: .cancel, handler: { _ in
observer.on(.error(error))
}))
alert.addAction(UIAlertAction(title: "替换", style: .default, handler: { _ in
observer.on(.next(1))
observer.on(.completed)
}))
self.present(alert, animated: true, completion: nil)
return Disposables.create {
alert.dismiss(animated: true, completion: nil)
}
}
}
.retry()

我们在 catchError 中返回了一个弹窗 Observable ,在重试的逻辑中向下发射了 Error ,替换的逻辑中向下发射了 1 。

并在后面接上了一个 retry

当选择重试的时候, retry 会接到这个错误,并重新订阅。这里需要注意的是,将 retry 直接接到弹窗O bservable 后面是错误的,此时将重新订阅弹窗 Observable ,永远触发不到最上层的随机数 Observable ,你将永远收到同一个错误。

此处错误代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.catchError { (error) -> Observable<Int> in
return Observable.create { [unowned self] observer in
let alert = UIAlertController(title: "遇到了一个错误,重试还是使用默认值 1 替换?", message: "错误信息:\(error.localizedDescription)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "重试", style: .cancel, handler: { _ in
observer.on(.error(error))
}))
alert.addAction(UIAlertAction(title: "替换", style: .default, handler: { _ in
observer.on(.next(1))
observer.on(.completed)
}))
self.present(alert, animated: true, completion: nil)
return Disposables.create {
alert.dismiss(animated: true, completion: nil)
}
}
.retry()
}

这里我还实现了一个二进制指数退避算法的错误处理,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension ObservableType {
public func retryWithBinaryExponentialBackoff(maxAttemptCount: Int, interval: TimeInterval) -> Observable<Self.E> {
return self.asObservable()
.retryWhen { (errorObservable: Observable<Swift.Error>) -> Observable<()> in
errorObservable
.scan((currentCount: 0, error: Optional<Swift.Error>.none), accumulator: { a, error in
return (currentCount: a.currentCount + 1, error: error)
})
.flatMap({ (currentCount, error) -> Observable<()> in
return ((currentCount > maxAttemptCount) ? Observable.error(error!) : Observable.just(()))
.delay(pow(2, Double(currentCount)) * interval, scheduler: MainScheduler.instance)
})
}
}
}

总结

RxSwift 为我们提供了基本的错误处理方法,我们还可以进行一定的组合得到我们特定的错误处理方案。

在使用处理各种错误时,但需要明确的是,我们想要 retry 或许 catch 哪个 Observable 的错误。