Swift Combine — zip和combineLatest的理解与使用

Publisher 上还有一些其他的操作,比如 zipcombineLatest,能让我们在时序上对控制多个 Publisher 的结果进行类似 andor 的合并,它们在构建复杂 Publisher 逻辑时也十分有用。

zip

Publisher 中的 zipSequencezip 相类似:它会把两个 (或多个) 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 中的值并对它们进行检查,比如说用户注册界面。

写在最后

对于 zipcombineLatest,它们有一个共同特点,那就是结合后的新 Publisher 所发出的数据是元组类型。对这两种操作,一种常见的模式是将结果的发出多元组数据的 Publisher 沿着响应链继续传递,使用我们之前看到过的各类 Operator 来获取能实际驱动 UI 和 app 状态的 Publisher

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值