Swift 组合运算符具有与 RxSwift 框架中的 `withLatestFrom` 相同的功能

Swift Combine operator with same functionality like `withLatestFrom` in the RxSwift Framework

我正在开发一个采用 MVVM 模式的 iOS 应用程序,使用 SwiftUI 设计视图和 Swift Combine 以便将我的视图及其各自的 ViewModel。 在我的一个 ViewModel 中,我为按下按钮创建了一个 Publisher(类型 Void),为 TextField 的内容创建了另一个(类型 String)。 我希望能够将我的 ViewModel 中的两个 Publisher 组合在一起,这样组合的 Publisher 仅在按钮 Publisher 发出事件时发出事件,同时从 String 发布者获取最新事件,因此我可以对 TextField 数据,每次用户按下按钮。所以我的虚拟机看起来像这样:

import Combine
import Foundation

public class MyViewModel: ObservableObject {
    @Published var textFieldContent: String? = nil
    @Published var buttonPressed: ()

    init() {
        // Combine `$textFieldContent` and `$buttonPressed` for evaulation of textFieldContent upon every button press... 
    }
}

两个发布者都被 SwiftUI 填充了数据,所以我将省略这部分,让我们假设两个发布者随着时间的推移收到一些数据。

来自 RxSwift 框架,我的 goto 解决方案是 withLatestFrom 运算符来组合两个可观察值。 然而,在 "Combining Elements from Multiple Publishers" 部分深入研究 Publisher 的 Apple 文档,我找不到类似的东西,所以我预计目前缺少这种运算符。

所以我的问题是:是否可以使用 Combine Framework 的现有运算符 -API 最终获得与 withLatestFrom 相同的行为?

为此设置一个内置运算符听起来不错,但您可以从已有的运算符中构建相同的行为,如果这是您经常做的事情,则很容易制作自定义运算符在现有运营商之外。

在这种情况下,我们的想法是将 combineLatest 与诸如 removeDuplicates 之类的运算符一起使用,以防止值在管道中传递,除非按钮已发出新值。例如(这只是操场上的测试):

var storage = Set<AnyCancellable>()
var button = PassthroughSubject<Void, Never>()
func pressTheButton() { button.send() }
var text = PassthroughSubject<String, Never>()
var textValue = ""
let letters = (97...122).map({String(UnicodeScalar([=10=]))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}

button.map {_ in Date()}.combineLatest(text)
    .removeDuplicates {
        [=10=].0 == .0
    }
    .map {[=10=].1}
    .sink { print([=10=])}.store(in:&storage)

typeSomeText()
typeSomeText()
typeSomeText()
pressTheButton()
typeSomeText()
typeSomeText()
pressTheButton()

输出的是"zed""zedaf"两个随机字符串。关键是每次我们调用 typeSomeText 时,文本都会沿着管道发送,但我们不会 收到 管道末端的文本,除非我们调用 pressTheButton.

这似乎是您所追求的。


您会注意到我完全忽略了按钮 发送的值是 。 (在我的示例中,它只是一个空值。)如果该值很重要,则更改初始映射以将该值作为元组的一部分包括在内,然后删除元组的日期部分:

button.map {value in (value:value, date:Date())}.combineLatest(text)
    .removeDuplicates {
        [=11=].0.date == .0.date
    }
    .map {([=11=].value, )}
    .map {[=11=].1}
    .sink { print([=11=])}.store(in:&storage)

这里的重点是 .map {([=18=].value, )} 行之后的内容与 withLatestFrom 将产生的内容完全相同:两者 发布者的最新的元组值。

作为@matt answer 的改进,这更方便withLatestFrom,在原始流中的同一事件上触发

已更新:修复 14.5

之前 iOS 版本中 combineLatest 的问题
extension Publisher {
  func withLatestFrom<P>(
    _ other: P
  ) -> AnyPublisher<(Self.Output, P.Output), Failure> where P: Publisher, Self.Failure == P.Failure {
    let other = other
      // Note: Do not use `.map(Optional.some)` and `.prepend(nil)`.
      // There is a bug in iOS versions prior 14.5 in `.combineLatest`. If P.Output itself is Optional.
      // In this case prepended `Optional.some(nil)` will become just `nil` after `combineLatest`.
      .map { (value: [=10=], ()) }
      .prepend((value: nil, ()))

    return map { (value: [=10=], token: UUID()) }
      .combineLatest(other)
      .removeDuplicates(by: { (old, new) in
        let lhs = old.0, rhs = new.0
        return lhs.token == rhs.token
      })
      .map { ([=10=].value, .value) }
      .compactMap { (left, right) in
        right.map { (left, [=10=]) }
      }
      .eraseToAnyPublisher()
  }
}

有点无法回答,但您可以这样做:

buttonTapped.sink { [unowned self] in
    print(textFieldContent)
}

这段代码很明显,不需要知道 withLatestFrom 是什么意思,尽管有必须捕获 self.

的问题

我想知道这是否是 Apple 工程师没有将 withLatestFrom 添加到核心 Combine 框架的原因。