-
- 1.1. 经典的RxSwift错误处理
- 1.1.1. Materialize
- 1.1.2. 我们的数据流
- 1.1.3. 我们的错误流
- 1.2. RxSwift 5 and CompactMap
- 1.3. The RxSwift 5 CompactMap Example
- 1.1. 经典的RxSwift错误处理
1. RxSwift: 使用CompactMap更好的处理错误
研究RxSwift足够长的时间,比如一周左右,你一定会遇到以下代码的一些变体:
class NoErrorViewModel {
var data: Observable<[String]>!
var dataService = DataService()
init(load: Observable<Void>) {
data = load
.flatMapLatest { [unowned self] _ in
self.dataService.load()
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)
.share()
}
}
这里,我们希望在每次发生加载事件时从数据服务返回一个字符串数组。比如说,在每个*viewWillAppear()*上。
所以,我们使用flatMapLatest()来包装我们的异步API调用为一个Observable,share()结果只是为了确保多个订阅数据不会因为同样的事件触发多个API调用,然后将结果分配给我们的视图模型的data observable 并返回主线程所以我们可以更新UI。一切都很好。
但是,为了使事件流保持存活状态,我们需要捕获可能发生的任何错误。如果我们不这样做,并且让错误从flatMapLatest() 流转到 parent stream,那么错误将沿链向下传播,并终止对数据的 any subscriptions。
这反过来又会阻止任何加载事件加载我们的数据。
这并不好,所以在flatMap中,我们可以看到调用数据服务之后的语句:
.catchErrorJustReturn()
它会捕获所有错误并返回一个空数组。
现在,这是一个很好的例子,但盲目地捕获并吃掉我们的错误并不能真正解决应用在现实环境中的问题。如果我们有一个错误,我们通常需要通知用户发生了一个错误,错误的性质,以及可以做什么。
静默失败和不返回任何数据实际上不是一个选项。你想启动你的银行应用程序来检查你的余额,但在你的账户里什么都看不到吗?
Proably not. So let’s do something about it.
1.1. 经典的RxSwift错误处理
所以现在我们想要返回我们的数据…如果找不到数据就返回一个错误。简而言之,我们的视图模型需要为我们的数据公开一个Observable<[String]>,并为一个错误公开一个Observable
One trigger, two observables, and we can’t let the error escape the flatMap. How?
Well, we could return a tuple of an error or our data, or we could map our data into a brand new Swift 5 Result… but we’re not. Instead, we’re going to use one of RxSwift’s lesser known operators: materialize.
1.1.1. Materialize
在flatMapLatest中使用materialize将结果从Observable<[String]>转换为Observable<Event<[String]>>,或者一个 event stream of events。

An event stream of events? That’s a bit meta, so let’s look at the code in action.
class MaterializingErrorViewModel {
var data: Observable<[String]>!
var error: Observable<String>!
var dataService = DataService()
init(load: Observable<Void>) {
let loading = load
.flatMapLatest { [unowned self] _ in
self.dataService.load().materialize()
}
.observeOn(MainScheduler.instance)
.share()
data = loading
.map { $0.element }
.filter { $0 != nil }
.map { $0! }
error = loading
.map { $0.error?.localizedDescription }
.filter { $0 != nil }
.map { $0! }
}
}
这里,我们用*flatMapLatest()中的materialize()函数替换了catchErrorJustReturn()*函数。
然后将该序列的最终结果赋给一个名为loading的临时变量。如果加载操作成功,则结果类型为Event.next<[String]>。如果是错误,则是Event.error(Swift.Error)。
接下来,我们将临时可观察流分成两个独立的可观察流:一个包含我们的数据,另一个包含我们的错误(如果有的话)。
1.1.2. 我们的数据流
我们使用.element访问隐藏在事件内部的数据,它返回Element类型的可选值,这是我们materialize数据值之前的原始类型。
本例中,将流的数据端转换为可选的字符串数组。然后,我们在nil上过滤可选值,最终的结果是,我们只让实际的数据值通过流。如果事件不包含数据,它就会被阻塞,而我们的可观察对象的任何订阅者都看不到任何东西。
最后,我们显式地打开我们知道存在的值,给我们第一个例子中的数据,一个Observable<[String]>。
1.1.3. 我们的错误流
我们对错误分支执行相同的操作,使用*$0.error?.localizedDescription*查看我们的materialized流是否包含错误事件,并在过程中提取由API返回的本地化错误。
然后,我们对数据执行相同的筛选和显式展开序列。result? 一个Observable
So. One trigger, a data observable, an error observable, and we didn’t let an error escape the flatMap and ruin our observable chain. Life is good.
Except…
我不知道你是怎么想的,但对我来说,在我们必须在可观察链的数据端和错误端执行的操作序列中,上面的代码似乎有点多余。
那么如何解决这个问题呢?
RxSwift 5 就是救星了。
1.2. RxSwift 5 and CompactMap
RxSwift 5为可观察流添加了一个新功能,它对应了Swift序列中添加的一个功能:compactMap。
在Swift中,对可选值数组使用compactMap()将返回一个新的值数组,其中所有可选值都被过滤掉了。换句话说,它可以转换Array<[String?]>到一个Array<[String]>。
在RxSwift中,compactMap()执行类似的函数,让我们将流的元素映射到可选值,然后过滤掉进程中产生的任何可选值(nil)。
1.3. The RxSwift 5 CompactMap Example
将compactMap()应用到最后一个示例,我们会得到以下结果……
class CompactMapErrorViewModel {
var data: Observable<[String]>!
var error: Observable<String>!
var dataService = DataService()
init(load: Observable<Void>) {
let loading = load
.flatMapLatest { [unowned self] _ in
self.dataService.load().materialize()
}
.observeOn(MainScheduler.instance)
.share()
data = loading
.compactMap { $0.element }
error = loading
.compactMap { $0.error?.localizedDescription }
}
}
在每种情况下,compactMap()用单个操作符替换数据和错误上的映射、过滤器、映射序列。
Much cleaner.
1.3.1. Regarding observeOn and Performance
有人可能会问*.observeon(MainScheduler.instance)*在共享加载序列中的位置。
我们移动observeOn()函数并在每个compactMap()操作下面添加一个不是更好吗?在我们将最终结果切换回主处理线程之前,这似乎会让更多的处理代码留在后台,你是对的。
它还将在我们的代码中添加两个相当繁重的后台到前台线程上下文切换。因为每个compactMap()函数都是非常轻量级的,所以在这个特定的实例中,我认为最好只执行一次上下文切换。
另一方面,如果我需要对加载的数组的每个元素执行一些操作,我可能会采取另一种方法,将observeOn()移动到每个分支。
class CompactMapOperationViewModel {
var data: Observable<[String]>!
var error: Observable<String?>!
var dataService = DataService()
init(load: Observable<Void>) {
let loading = load
.flatMapLatest { [unowned self] _ in
self.dataService.load().materialize()
}
.share()
data = loading
.compactMap { $0.element?.map { "Hello, \($0)" }
.observeOn(MainScheduler.instance)
error = loading
.map { $0.error?.localizedDescription }
.observeOn(MainScheduler.instance)
}
如果您有敏锐的眼光,您可能还会注意到,现在可选的错误可观察对象上的最后一个compactMap()被改成了map()。
在这个修改过的案例中,我们让nil错误字符串事件传递,这反过来让我们在数据成功加载时清除视图控制器错误标签中的任何之前的错误消息。
这两种情况都应该清楚地表明,您需要考虑您的代码和UI需求以及正在执行的操作,并相应地调整您的编码模式。
1.3.2. CompactMap / Unwrap Bonus Round
你可能遇到过一个RxSwift社区扩展库,叫做RXSwiftExt。如果是这样,您可能已经看到甚至使用过名为unwrap()的RXSwiftExt操作符。
Unwrap :“接受一个可选元素序列并返回一个非可选元素序列,过滤掉任何nil值。”
Unwrap是RxSwift中的一个方便的小函数,考虑到Swift语言中使用可选选项的流行程度。事实上,它是如此方便,以至于经常有人提议将其添加到核心RxSwift语言本身中。
然而,看看实现内部,你会看到一些熟悉的东西……
public func unwrap<T>() -> Observable<T> where Element == T? {
return self.filter { $0 != nil }.map { $0! }
}
是的。这和我们之前看到的过滤器,映射,显式展开序列是一样的。 这意味着在RxSwift 5中你可以这样做:
let stringIsRequired: Observable<String> = optionalString
.compactMap { $0 }
是的。使用compactMap(),相当于unwrap()的功能现在实际上是RxSwift核心的一部分。
这就是它。RxSwift 5中添加了compactMap(),这让我们可以编写更少的代码,并且性能和内存效率更高,用一个RxSwift操作替换了三个操作。
作为额外的好处,我们现在可以轻松地unwrap()可选的事件流,而不需要使用其他库或将扩展添加到我们自己的代码库中。
Enjoy.