Publisher
上还有一些其他的操作,比如 zip
和 combineLatest
,能让我们在时序上对控制多个 Publisher
的结果进行类似 and
和 or
的合并,它们在构建复杂 Publisher
逻辑时也十分有用。
zip
Publisher
中的 zip
和 Sequence
的 zip
相类似:它会把两个 (或多个) Publisher
事件序列中在同一 index
位置上的值进行合并,也就是说,Publisher1
中的第一个事件和 Publisher2
中的第一个事件结对合并,Publisher1
中的第二个事件和 Publisher2
中的第二个事件合并,以此类推:
比如上图中:
Publisher1 中第一个位置值为 1,在等待到 Publisher2 的第一个值 A 到来时,两者进行合并,在输出的 Publisher 中形成 (1, A)。
两个 Publisher 第二个位置的 2 和 B 也同理。
在 Publisher2 中 C 和 D 连续发生时,由于 Publisher1 里对应序列位置的事件还没有发生,因此并不会输出在最终的 Publisher 中,直到 Publisher1 里的 3 和 4 被发布。
最后 5 在 Publisher2 里并没有对应位置的 Output 事件,因此它被最终的 Publisher 忽略了。
下面的代码全完验证了上面的结论:
import SwiftUI
import Combine
class ZipViewModel: ObservableObject {
private var cancellable = Set<AnyCancellable>()
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
@Published var receivedValues: [(Int, String)] = []
func zipSample() {
numbersPub
.zip(lettersPub)
.sink(receiveValue: { value in
print("Received value: \(value)")
self.receivedValues.append(value)
})
.store(in: &cancellable)
}
func sendNumber(_ number: Int) {
print("Send number: \(number)")
numbersPub.send(number)
}
func sendLetter(_ letter: String) {
print("Send letter: \(letter)")
lettersPub.send(letter)
}
}
struct ZipDemo: View {
@StateObject private var viewModel = ZipViewModel()
@State private var number: String = ""
@State private var letter: String = ""
var body: some View {
VStack {
VStack {
TextField("Enter number...", text: $number)
TextField("Enter letter...", text: $letter)
}
.textFieldStyle(.roundedBorder)
.padding()
HStack {
Button("Send number") {
if let intNumber = Int(number) {
viewModel.sendNumber(intNumber)
}
}
.frame(maxWidth: .infinity)
Button("Send letter") {
if !letter.isEmpty {
viewModel.sendLetter(letter)
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(BorderedProminentButtonStyle())
}
.onAppear {
viewModel.zipSample()
}
}
}
打印结果如下:
Send number: 1
Send letter: A
Received value: (1, "A")
Send number: 2
Send letter: B
Received value: (2, "B")
Send letter: C
Send letter: D
Send number: 3
Received value: (3, "C")
Send number: 4
Received value: (4, "D")
Send number: 5
zip
在时序语义上更接近于“当…且…”,当 Publisher1
发布值,且 Publisher2
发布值时,将两个值合并,作为新的事件发布出去。在实践中,zip
经常被用在合并多个异步事件的结果,比如同时发出了多个网络请求,希望在它们全部完成的时候把结果合并在一起。
combineLatest
和 zip
相反,combineLatest
的语义接近于“当…或…”,当 Publisher1
发布值,或者 Publisher2
发布值时,将两个值合并,作为新的事件发布出去。
不论是哪个 Publisher
,只要发生了新的事件,combineLatest
就把新发生的事件值和另一个 Publisher
中当前的最新值合并。
combineLatest
操作的图示如下:
上图中,除了首个元素 1 以外,其余的每个事件值,不论是从哪个 Publisher
发出的,都会被用来进行组合和输出。
比如在 Publisher2 输出 A 时,combineLatest
将使用这个 A 和 Publisher1 的当前最终值1 组合出结果1A。同样,随后 Publisher1 发布的 2 也和 Publisher2 的最终值 A 组合得到 2A。这个过程持续下去,直到两个 Publisher 都结束。
下面的代码验证了这一结论:
import SwiftUI
import Combine
class CombineLatestViewModel: ObservableObject {
private var cancellable = Set<AnyCancellable>()
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
@Published var receivedValues: [(Int, String)] = []
func zipSample() {
numbersPub
.combineLatest(lettersPub)
.sink(receiveValue: { value in
print("Received value: \(value)")
self.receivedValues.append(value)
})
.store(in: &cancellable)
}
func sendNumber(_ number: Int) {
print("Send number: \(number)")
numbersPub.send(number)
}
func sendLetter(_ letter: String) {
print("Send letter: \(letter)")
lettersPub.send(letter)
}
}
struct CombineLatestDemo: View {
@StateObject private var viewModel = CombineLatestViewModel()
@State private var number: String = ""
@State private var letter: String = ""
var body: some View {
VStack {
VStack {
TextField("Enter number...", text: $number)
TextField("Enter letter...", text: $letter)
}
.textFieldStyle(.roundedBorder)
.padding()
HStack {
Button("Send number") {
if let intNumber = Int(number) {
viewModel.sendNumber(intNumber)
}
}
.frame(maxWidth: .infinity)
Button("Send letter") {
if !letter.isEmpty {
viewModel.sendLetter(letter)
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(BorderedProminentButtonStyle())
}
.onAppear {
viewModel.zipSample()
}
}
}
打印结果为:
Send number: 1
Send letter: A
Received value: (1, "A")
Send number: 2
Received value: (2, "A")
Send letter: B
Received value: (2, "B")
Send letter: C
Received value: (2, "C")
Send letter: D
Received value: (2, "D")
Send number: 3
Received value: (3, "D")
Send number: 4
Received value: (4, "D")
Send number: 5
Received value: (5, "D")
在实践中,combineLatest
被用来处理多个可变状态,在其中某一个状态发生变化时,获取这些全部状态的最新值。比如你的 UI 上有多个 TextField,你可能想要在其中某一个值变动时获取到所有 TextField 中的值并对它们进行检查,比如说用户注册界面。
写在最后
对于 zip
和 combineLatest
,它们有一个共同特点,那就是结合后的新 Publisher
所发出的数据是元组类型。对这两种操作,一种常见的模式是将结果的发出多元组数据的 Publisher 沿着响应链继续传递,使用我们之前看到过的各类 Operator 来获取能实际驱动 UI 和 app 状态的 Publisher
。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。