原文:How to handle errors in RxSwift
最近,MVVM 成为 iOS 应用程序非常流行的架构设计。尤其是当 RxSwift 开始越来越受欢迎的时候。在 RxMVVM 中,大部分属性都是通过 Observables 表达的。
不过,每当观察对象收到 error
或 completed
事件时,它们就会终止。终止意味着 Observable
的订阅者不会收到任何新消息。当我开始学习 Rx 时,我并没有意识到这一规则的后果。
你在错误处理方面遇见过问题吗?您的 Observable
是否意外终止,导致按钮不再发送点击事件?这就是本文的主题。祝您阅读愉快 📚💪
Errors - 示例
当用户点击按钮时,移动应用程序通常会执行一些 API 请求。我们的示例将涵盖这种情况:
点击 success
时会调用假 API 请求,并得到成功的答复。同样,点击 failure
按钮也会伪造错误。点击按钮应增加计数。
这就是我想通过此图实现的目标:
successTap -s-----s--s-----s---------->
failureTap ----f---f-----f----f------->
buttonTaps<Bool> -T--F--TF-F---F-T--F------->
response --V--E--VE-V---E-V--E------>
(using flatMap)
where:
's' and 'f' - success or failure button tap
'T' and 'F' - true or false
'V' - success response
'E' - failure response (error)
编码时间 - 第 1 次尝试
让我们编写一些代码。您需要将点击成功按钮映射为 true
事件,将点击失败按钮映射为 false
事件。然后,将它们 merge()
为一个 Observable
:
如果这是你第一次使用 Rx 和 merge()
、map()
或 flatMap()
,而且感觉很陌生,请先阅读RxSwift 编程思想。我将在那里介绍如何以反应式方式思考,以及如何使用。
事实上,点击 success
按钮确实会增加成功次数。但是,只要点击 failure
按钮,整个可观察对象链就会自行 dispose。接下来,点击 success
按钮不再会增加成功次数🙀。
你会问为什么?
当 performAPICall
失败时,它会返回一个错误事件(与真正的 API 调用相同)。由于我们使用的是 flatMap
,因此内部 Observable
中的所有 next
和 error
都会传递到主序列中。
因此,主 Observable
序列会收到一个错误事件,同时也会终止💀⚰。
RxSwift 与错误 -- 如何处理?
有时,错误正是你所期望发生的。以登录表单为例。如果密码与给定的电子邮件不匹配,服务器就会返回错误。这是一个预料之中的错误,天哪,这下可好,错误来了!👌
如果错误不是异常,就不应该结束 Observable
序列。要做到这一点,你的 API 调用应返回 Observable<Result<T>>
。将 Result<T>
作为 next
事件不会终止主 Observable
序列。
不过,还有一种更简单的方法。RxSwiftExt 提供了 materialize
操作符。它将 Observable<T>
转换为 Observable<Event<T>>
,后者有两个附加操作符:
elements()
返回Observable<T>
errors()
返回Observable<Error>
有了这两个观察项,您就可以按照自己的意愿处理 API 错误了:
😱 - performAPICall()
被调用两次
上述解决方案正如我们所期望的那样有效,但其中存在一个错误。每当你按下任何按钮时,都会调用两次 performAPICall()
。要查看这一点,必须在 performAPICall()
中设置一个断点。
你可能会说这在我们的示例中没什么大不了的,但在现实生活中,调用一个方法 2 次会向服务器发送 2 个请求,这就不好了。要解决这个问题,需要在 result
Observable 中使用 share()
操作符。其余部分不变:
何去何从?
使用 RxSwift 扩展为用户界面提供信息时,处理错误并不像您最初想象的那样简单。即使错误来自内部的 flatMap
,错误事件也会破坏 Observable
。
通常,您希望将错误通知给用户。要做到这一点,必须将错误视为预期会发生的事情,而不是异常。我建议使用 RxSwiftExt 中的 materialize()
,但不要忘记使用 share()
🙂。您不会希望向 API 发送 2 个请求😉。
您可以在此处找到项目示例。
如果你想了解有关 share()
操作符的更多信息,有一篇很不错的文章。
GitHub 上的一个问题提到了更多关于错误和不存在普遍错误的观点。该演讲令人大开眼界。我认为值得一读。
您喜欢这篇文章吗?请点击下面的按钮进行分享。🍺
References
标题图片 - dribbble.com - Artur Martynowski @ All in Mobile
附:本文源码
//
// ViewController.swift
// RxErrorHandling
//
// Created by Adam Borek on 01.03.2017.
// Copyright © 2017 Adam Borek. All rights reserved.
//
import UIKit
import RxSwift
import RxSwiftExt
import RxCocoa
struct SampleError: Swift.Error {}
class ViewController: UIViewController {
let disposeBag = DisposeBag()
@IBOutlet weak var successessCountLabel: UILabel!
@IBOutlet weak var failuresCountLabel: UILabel!
@IBOutlet weak var successButton: UIButton!
@IBOutlet weak var failureButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//failWillCloseTheStream()
usingMaterialize()
}
private func failWillCloseTheStream() {
let successesCount = Observable
.of(successButton.rx.tap.map { true }, failureButton.rx.tap.map { false })
.merge()
.flatMap { [unowned self] performWithSuccess in
return self.performAPICall(shouldEndWithSuccess: performWithSuccess)
}.scan(0) { accumulator, _ in
return accumulator + 1
}.map { "\($0)" }
successesCount.bindTo(successessCountLabel.rx.text)
.disposed(by: disposeBag)
}
private func usingMaterialize() {
let result = Observable
.of(successButton.rx.tap.map { true }, failureButton.rx.tap.map { false })
.merge()
.flatMap { [unowned self] performWithSuccess in
return self.performAPICall(shouldEndWithSuccess: performWithSuccess)
.materialize()
}.share()
result.elements()
.scan(0) { accumulator, _ in
return accumulator + 1
}.map { "\($0)" }
.bindTo(successessCountLabel.rx.text)
.disposed(by: disposeBag)
result.errors()
.scan(0) { accumulator, _ in
return accumulator + 1
}.map { "\($0)" }
.bindTo(failuresCountLabel.rx.text)
.disposed(by: disposeBag)
}
private func performAPICall(shouldEndWithSuccess: Bool) -> Observable<Void> {
if shouldEndWithSuccess {
return .just(())
} else {
return .error(SampleError())
}
}
}