正如您现在可能已经意识到的那样,操作符基本上就是您用来操纵 Combine 发布者的词汇。您知道的操作符越多,您对数据的控制就越好。 在前一章中,您学习了如何使用值并将它们转换为其他值——绝对是您日常工作中最有用的运算符类别之一。 但是当您想限制发布者发出的值或事件,并且只使用其中的一些时会发生什么?本章是关于如何使用一组特殊的运算符来做到这一点的:过滤运算符! 幸运的是,这些运算符中有许多在 Swift 标准库中具有相同的名称,因此如果您能够过滤本章的某些内容,请不要感到惊讶。
过滤基础
第一部分将涉及过滤的基础知识:有条件地决定将哪些值传递给用户。
最简单的方法是使用运算符——filter,它有一个返回 Bool 的闭包,返回为true的数据会被留下来,false的数据会被过滤掉
在playground中加入如下代码:
// 1
let numbers = (1...10).publisher
// 2
numbers
.filter { $0.isMultiple(of: 3) }
.sink(receiveValue: { n in
print("\(n) is a multiple of 3!")
})
.store(in: &subscriptions)
上述代码中,我们实现了:
- 创建一个新的发布者,它是Sequence类型的发布者,它将发出有限数量的值——1 到 10。
- 使用
filter运算符,在闭包中进行逻辑判断,这里用的判断条件是:数据是否是3的倍数。
运行playground,得到结果
3 is a multiple of 3!
6 is a multiple of 3!
9 is a multiple of 3!
在你的应用程序的生命周期中,很多时候你会遇到发布者连续发布相同的值,而这些值你可能想要忽略。例如,如果用户连续键入“a”五次,然后键入“b”,您可能希望忽略过多的“a”。
Combine 为任务提供了完美的操作符:removeDuplicates
请注意,您不必为此运算符提供任何参数。 removeDuplicates 自动适用于符合 Equatable协议的任何值,包括 String类型。
请看如下代码:
// 1
let words = "hey hey there! want to listen to mister mister ?"
.components(separatedBy: " ")
.publisher
// 2
words
.removeDuplicates()
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
- 将一个句子分成一组单词(例如,[String]),然后创建一个新的发布者来发布这些单词。
- 将 removeDuplicates() 应用于您的词发布者。
运行playground,得到结果:
hey
there!
want
to
listen
to
mister
?
可以看到,removeDuplicates过滤掉了重复的hey和mister
不符合 Equatable 的值怎么办?好吧,removeDuplicates 有另一个重载,它接受一个包含两个值的闭包,从中您将返回一个 Bool 来指示值是否相等
Compacting and ignoring(扁平化和忽略)
compactMap
很多时候,您会发现自己与发布 Optional 值的发布者打交道。或者更常见的是,您希望对可能返回 nil 的值执行一些操作,Swift标准库中有compactMap 可以完成这项工作,同时也有一个同名的Operators可以用在Combine中。 他的主要作用就是过滤掉nil值。
在playground中加入如下代码:
// 1
let strings = ["a", "1.24", "3",
"def", "45", "0.23"].publisher
// 2
strings
.compactMap { Float($0) }
.sink(receiveValue: {
// 3
print($0)
})
.store(in: &subscriptions)
- 创建一个字符串数组的发布者
- 使用 compactMap 尝试从每个单独的字符串初始化 Float。如果 Float 的初始化程序不知道如何转换提供的字符串,则返回 nil。这些 nil 值会被 compactMap 操作符自动过滤掉。
- 仅打印已成功转换为浮点数的字符串
运行playground,得到结果:
1.24
3.0
45.0
0.23
ignoreOutput
忽略发布者的Output
如上图所示,发出哪些值或发出多少个值并不重要,因为它们都被忽略了;您只需将完成事件推送给订阅者。
在playground中加入如下代码:
// 1
let numbers = (1...10_000).publisher
// 2
numbers
.ignoreOutput()
.sink(receiveCompletion: { print("Completed with: \($0)") },
receiveValue: { print($0) })
.store(in: &subscriptions)
- 创建一个发布从 1 到 10,000 的 10,000 个值的发布者。
- 添加 ignoreOutput 操作符,它忽略所有值并且只向消费者发出完成事件。
运行playground,你会发现打印信息为:
Completed with: finished
sink的receiveValue这里没有任何数据被接收到,因为ignoreOutput操作符忽略掉了所有发布的数据,只是将发布结束的状态通知了订阅者。
查询数据
first
这个运算符很有趣,因为它很懒惰,它在发布值中查找与条件匹配的值,一旦找到匹配项,它就会取消订阅并完成。
![[img1124.jpg]]
在playground中加入如下代码:
// 1
let numbers = (1...9).publisher
// 2
numbers
.first(where: { $0 % 2 == 0 })
.sink(receiveCompletion: { print("Completed with: \($0)") },
receiveValue: { print($0) })
.store(in: &subscriptions)
- 创建一个新的发布者,发出从 1 到 9 的数字。
- 使用 first(where:) 运算符来查找第一个发出的偶数值
运行playground,得到结果:
2
Completed with: finished
first操作符找到第一个符合条件的数据2之后,就完成了订阅。
现在我们通过加入print调试操作符来看下整个流程发生了什么。在numbers和.first(where:)两行之间增加一行代码
.print("numbers")
再次运行playground,得到结果:
numbers: receive subscription: (1...9)
numbers: request unlimited
numbers: receive value: (1)
numbers: receive value: (2)
`numbers: receive cancel`
2
Completed with: finished
如您所见,一旦 first(where:) 找到匹配的值,它就会通过订阅发送取消消息,从而导致上游停止发送值。
last
与 first(where:) 不同,这个操作符是贪婪的,因为它必须等待发布者完成发送值才能知道是否找到了匹配的值。因此,上游数据必须是有限的。
在playground中加入如下代码:
// 1
let numbers = (1...9).publisher
// 2
numbers
.last(where: { $0 % 2 == 0 })
.sink(receiveCompletion: { print("Completed with: \($0)") },
receiveValue: { print($0) })
.store(in: &subscriptions)
- 创建一个发布 1 到 9 之间数字的发布者。
- 使用 last(where:) 运算符查找最后发出的偶数值
运行playground,得到结果:
8
Completed with: finished
现在我们将代码替换为如下代码,可以更清晰的理解为何称last为贪婪:
let numbers = PassthroughSubject<Int, Never>()
numbers
.last(where: { $0 % 2 == 0 })
.sink(receiveCompletion: { print("Completed with: \($0)") },
receiveValue: { print($0) })
.store(in: &subscriptions)
numbers.send(1)
numbers.send(2)
numbers.send(3)
numbers.send(4)
numbers.send(5)
这里我们使用PassthroughSubject这个符合Subject协议的Publisher,来通过 send 手动发布一些数据。
运行playground,你会发现没有任何打印信息。
正如预期的那样,由于发布者永远不会完成,因此无法确定与条件匹配的最后一个值。
现在我们在代码最后加上一行,来手动发布一个完成的信息
numbers.send(completion: .finished)
再次运行playground,得到结果:
4
Completed with: finishedb