Chapter 5: Filtering Operators
学习一项新技术堆栈有点像建造摩天大楼。在你能到达天空之前,你必须建立一个坚实的基础。到目前为止,您已经建立了对RxSwift的基本理解,现在是时候开始建立您的知识库和技能集了,一次一个水平。
这一章将告诉你RxSwift的过滤操作符,你可以用它来对发出的事件应用条件约束,这样订阅者就只接收到它想要处理的元素。如果你曾经在Swift标准库中使用过filter(_:)方法,那么你已经成功了一半。如果没有,没有烦恼;在这一章结束时,你将成为这方面的专家。
Getting started
本章的初始项目名为RxPlayground。在project文件夹中运行./bootstrap.sh后,Xcode会打开。在项目导航器中选择RxSwiftPlayground,你就可以开始操作了。
Ignoring operators
您将直接进入并查看RxSwift中一些有用的过滤操作符,从ignoreElements开始。如下面的弹珠图所示,ignoreElements将忽略所有接下来的事件。但是,它将允许停止事件通过,例如已完成或错误事件。
允许停止事件通过通常是在所有弹珠图中隐含的。这次它被显式地调用了,因为所有的ignoreElements都会让它通过。

注意:到目前为止,您已经看到了用于类型的弹珠图。这种弹珠图的形式可以帮助您可视化操作人员是如何工作的。最上面一行是被订阅的可观察对象。框表示操作符及其参数,底线是订阅者,或者更具体地说,订阅者在操作符完成其工作后将接收到的内容。
要查看ignoreElements的实际作用,请将这个例子添加到你的游乐场:
example(of: "ignoreElements") {
// 1
let strikes = PublishSubject<String>()
let disposeBag = DisposeBag()
// 2
strikes
.ignoreElements()
.subscribe { _ in
print("You're out!")
}
.disposed(by: disposeBag)
}
你是这样做的:
- Create a strikes subject.
- Subscribe to all strikes’ events, but ignore all next events by using ignoreElements.
注意:如果您不太了解击球、击球手和棒球游戏,那么当您决定从编程中休息一下时,您可以阅读以下内容:https://simple.wikipedia.org/wiki/Baseball。
如果你只希望在可观察对象结束时(通过完成或错误事件)得到通知,那么ignoreElements操作符非常有用。将以下代码添加到示例中:
strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")
尽管这个击球手似乎不能击中谷仓的大侧面,显然已经出局了,但什么也没打印出来,因为你忽略了接下来的所有事件。您可以自行向该主题添加一个已完成的事件,以便通知订阅者。添加以下代码:
strikes.onCompleted()
现在订阅者将收到已完成的事件,并打印出任何击球手都不想听到的口号:
--- Example of: ignoreElements ---
You're out!
善于发现的读者可能会注意到ignoreElements实际上返回一个Completable,这很有意义,因为它只会发出一个完成或错误事件。
有时候,你可能只想处理由可观察对象发出的第n(序数)个元素,比如第三次攻击。为此,您可以使用elementAt,它接受您想要接收的元素的索引,并忽略其他一切。在弹珠图中,elementAt被传递了索引1,所以它只允许通过第二个元素。

添加这个新示例:
example(of: "elementAt") {
// 1
let strikes = PublishSubject<String>()
let disposeBag = DisposeBag()
// 2
strikes
.elementAt(2)
.subscribe(onNext: { _ in
print("You're out!")
})
.disposed(by: disposeBag)
}
比赛详情:
- You create a subject.
- You subscribe to the next events, ignoring all but the 3rd next event, found at index 2.
现在您可以简单地在主题中添加新的打击,您的订阅会让您知道击球手何时三振出局。添加此代码:
strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")
“Hey batta, batta, batta — swing batta!”
--- Example of: elementAt ---
You're out!
关于元素(at:)的一个有趣事实是:一旦元素在提供的索引处发出,订阅就终止。
ignoreElements和elementAt过滤由可观察对象发出的元素。当您的过滤需要超过全部或一个时,请使用filter作符。它接受一个谓词闭包并将其应用于发出的每个元素,只允许通过谓词解析为true的元素。
查看这个弹珠图,其中只有1和2允许通过,因为过滤器的谓词只允许小于3的元素。

将这个例子添加到你的操场:
example(of: "filter") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5, 6)
// 2
.filter { $0.isMultiple(of: 2) }
// 3
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
从头再来:
- You create an observable of some predefined integers.
- You use the filter operator to apply a conditional constraint to prevent odd numbers from getting through.
- You subscribe and print out the elements that pass the filter predicate.
应用这个过滤器的结果是只打印偶数:
--- Example of: filter ---
2
4
6
Skipping operators
如果想要跳过特定数量的元素,请使用skip操作符。它允许您忽略前n个元素,其中n是作为参数传递的数字。这个弹珠图显示skip被传递为2,因此它忽略了前2个元素。

将这个新例子添加到你的操场:
example(of: "skip") {
let disposeBag = DisposeBag()
// 1
Observable.of("A", "B", "C", "D", "E", "F")
// 2
.skip(3)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
有了这个代码,你:
- Create an observable of letters.
- Use skip to skip the first 3 elements and subscribe to next events.
跳过前3个元素后,只打印D、E和F:
--- Example of: skip ---
D
E
F
skip操作符有一小家族。与filter类似,skipWhile允许包含一个谓词来确定跳过了什么。但是,与筛选订阅生命周期元素的filter不同,skipWhile只会跳过某些内容,然后让其他内容从该点开始通过。
对于skipWhile,返回true将导致跳过元素,返回false将允许它通过。它是filter的反义词。
在这个弹球图中,1被阻止,因为1% % 2等于1,但是2被允许通过,因为它失败了谓词,而3(以及其他所有内容)通过了,因为skipWhile不再跳过。

将这个新例子添加到你的操场:
example(of: "skipWhile") {
let disposeBag = DisposeBag()
// 1
Observable.of(2, 2, 3, 4, 4)
// 2
.skipWhile { $0.isMultiple(of: 2) }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
Here’s what you did:
- Create an observable of integers.
- Use skipWhile with a predicate that skips elements until an odd integer is emitted.
记住,skip只会跳过元素,直到第一个元素被允许通过,然后剩下的所有元素都被允许通过。这个例子输出:
--- Example of: skipWhile ---
3
4
4
例如,如果您正在开发一个保险索赔应用程序,您可以使用skipWhile来拒绝承保,直到满足免赔额。
到目前为止,您已经根据静态条件进行了过滤。如果你想根据另一个可观察对象动态地过滤元素呢?有几个操作符可供选择。
第一个是skipUntil,它将继续跳过源可观察对象(你正在订阅的对象)中的元素,直到其他触发可观察对象发出。在这个弹珠图中,skipUntil忽略顶部一行的源可观察对象发出的元素,直到第二行的触发器可观察对象发出下一个事件。然后它会停止skipping,从那一刻开始让所有的东西都通过。

添加这个例子来看看skipUntil是如何在代码中工作的:
example(of: "skipUntil") {
let disposeBag = DisposeBag()
// 1
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
// 2
subject
.skipUntil(trigger)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
In this code, you:
- Create a subject to model the data you want to work with, and another subject to act as a trigger.
- Use skipUntil and pass the trigger subject. When trigger emits, skipUntil stops skipping.
在主题上添加几个接下来的事件:
subject.onNext("A")
subject.onNext("B")
没有打印,因为你跳过了。现在在触发器上添加一个新的next事件:
trigger.onNext("X")
这将导致skipUntil停止跳过。从这一点开始,所有的元素都可以通过。添加下一个事件到主题:
subject.onNext("C")
果然,它被打印出来了:
--- Example of: skipUntil ---
C
Taking operators
Taking是skipping的反义词。当你想获取元素时,RxSwift可以满足你的需求。您将学习的第一个取操作符是take,正如此弹珠图所描述的,它将取指定的元素数中的第一个。

将这个示例添加到您的游乐场中,以探索第一个take操作符:
example(of: "take") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5, 6)
// 2
.take(3)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an observable of integers.
- Take the first 3 elements using take.
What you take is what you get. The output is:
--- Example of: take ---
1
2
3
takeWhile操作符的工作原理与skipWhile类似,只是您使用的是taking而不是skipping。

此外,如果希望引用要发出的元素的索引,可以使用enumerated操作符。它生成包含可观察对象中每个被触发元素的索引和元素的元组,类似于Swift标准库中的enumerated方法的工作方式。
在你的操场上输入这个新例子:
example(of: "takeWhile") {
let disposeBag = DisposeBag()
// 1
Observable.of(2, 2, 4, 4, 6, 6)
// 2
.enumerated()
// 3
.takeWhile { index, integer in
// 4
integer.isMultiple(of: 2) && index < 3
}
// 5
.map(\.element)
// 6
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
From the top, you:
- Create an observable of integers.
- Use the enumerated operator to get tuples containing the index and value of each element emitted.
- Use the takeWhile operator, and destructure the tuple into individual arguments.
- Pass a predicate that will take elements until the condition fails.
- Use map — which works just like the Swift Standard Library map — to reach into the tuple returned from takeWhile and get the element.
- Subscribe to and print out next elements.
结果是,您只接收到整数为偶数的元素,直到元素的索引为3或更大。
--- Example of: takeWhile ---
2
2
4
与takeWhile相反,有一个takeUntil操作符,该操作符接受元素,直到满足谓词。它的第一个参数还接受一个behavior参数,该参数指定您是希望包含还是排除与谓词匹配的最后一个元素。

将这个新例子添加到你的操场,看看它是如何工作的:
example(of: "takeUntil") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5)
// 2
.takeUntil(.inclusive) { $0.isMultiple(of: 4) }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an Observable of sequential integers.
- Use the takeUntil operator with inclusive behavior.
这段代码输出传递谓词的元素之前的元素,并包括这些元素:
--- Example of: takeUntil ---
1
2
3
4
现在,将行为从.包容性(.inclusive )改为.排他性(.exclusive),再次运行游乐场。这一次,传递谓词的元素被排除了:
--- Example of: takeUntil ---
1
2
3
和skipUntil一样,还有一个变种的takeUntil也适用于触发器可观察对象。下面的弹珠图显示了takeUntil从源可观察对象中提取,直到触发可观察对象发出一个元素。

添加这个新示例,就像前面创建的skipUntil示例一样:
example(of: "takeUntil trigger") {
let disposeBag = DisposeBag()
// 1
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
// 2
subject
.takeUntil(trigger)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
// 3
subject.onNext("1")
subject.onNext("2")
}
Here’s what you did:
- Create a primary subject and a trigger subject.
- Use takeUntil, passing the trigger that will cause takeUntil to stop taking once it emits.
- Add a couple of elements onto subject.
元素会被打印出来,因为takeUntil处于taking模式:
--- Example of: takeUntil trigger ---
1
2
现在在触发器上添加一个元素,然后在主题上添加另一个元素:
trigger.onNext("X")
subject.onNext("3")
X停止抓取,所以3不允许通过,没有更多的打印。
有一种方法可以使用takeUntil和RxCocoa库中的API来释放订阅,而不是将其添加到一个释放包中。你将在第三节学习RxCocoa,“iOS Apps with RxCocoa”一般来说,避免内存泄漏的安全方法是总是将订阅添加到一个释放包中。然而,为了完整起见,这里有一个你如何在RxCocoa中使用takeUntil的例子——不要把这个带入你的操场,因为它无法编译:
_ = someObservable
.takeUntil(self.rx.deallocated)
.subscribe(onNext: {
print($0)
})
在上面的代码中,self的释放是导致takeUntil停止taking的触发器,self通常是一个视图控制器或视图模型。
Distinct operators
接下来的两个操作符可以防止重复的连续项通过。如图所示,distinctUntilChanged只能防止相邻的重复项,所以第二个1可以通过。

将这个新例子添加到你的操场:
example(of: "distinctUntilChanged") {
let disposeBag = DisposeBag()
// 1
Observable.of("A", "A", "B", "B", "A")
// 2
.distinctUntilChanged()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
What you do with this code:
- Create an observable of letters.
- Use distinctUntilChanged to prevent sequential duplicates from getting through.
distinctUntilChanged操作符只能防止连续的重复,所以第二个A和第二个B会被阻止,因为它们与前一个元素相等。但是,第三个A被允许通过,因为它不等于它的前一个元素。打印结果如下:
--- Example of: distinctUntilChanged ---
A
B
A
这些是String的实例,它们符合Equatable。但是,你可以选择使用distinctUntilChanged(_:)来提供你自己的自定义逻辑来测试是否相等;传递的参数是比较器。
在下面的弹珠图中,将比较具有名为value属性的对象是否基于value进行相等性。

将这个稍微复杂一点的例子添加到你的操场上:
example(of: "distinctUntilChanged(_:)") {
let disposeBag = DisposeBag()
// 1
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
// 2
Observable<NSNumber>.of(10, 110, 20, 200, 210, 310)
// 3
.distinctUntilChanged { a, b in
// 4
guard
let aWords = formatter
.string(from: a)?
.components(separatedBy: " "),
let bWords = formatter
.string(from: b)?
.components(separatedBy: " ")
else {
return false
}
var containsMatch = false
// 5
for aWord in aWords where bWords.contains(aWord) {
containsMatch = true
break
}
return containsMatch
}
// 6
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
From the top, you:
- 创建一个数字格式化程序来拼写出每个数字。
- 创建一个NSNumbers的可观察对象,而不是int,这样你就不必在接下来使用formatter时转换整数。
- 使用distinctUntilChanged(_😃,它接受一个接收每个序列元素对的谓词闭包。
- 使用guard有条件地绑定由一个空格分隔的元素组件,否则返回false。
- 迭代第一个数组中的每个单词,看看它是否包含在第二个数组中。
- 根据您提供的比较逻辑订阅并打印出被认为是不同的元素。
因此,只打印不同的整数,考虑到在每对整数中,一个不包含另一个的任何单词组成部分。
--- Example of: distinctUntilChanged(_:) ---
10
20
200
distinctUntilChanged(_:)操作符在你想清楚地防止不符合Equatable类型的重复时也很有用。