【问题标题】:Swift Combine operator with same functionality like `withLatestFrom` in the RxSwift FrameworkSwift 将运算符与 RxSwift 框架中的 `withLatestFrom` 等功能相结合
【发布时间】:2020-09-09 13:36:48
【问题描述】:

我正在开发一个采用 MVVM 模式的 iOS 应用程序,使用 SwiftUI 设计视图并使用 Swift Combine 将我的视图与它们各自的视图模型粘合在一起。 在我的一个 ViewModel 中,我为按下按钮创建了一个 Publisher(类型为 Void),为一个 TextField(类型为 String)的内容创建了另一个。 我希望能够在我的 ViewModel 中组合两个发布者,这样组合的发布者只在按钮发布者发出事件时发出事件,同时从字符串发布者那里获取最新事件,所以我可以对 @ 进行某种评估987654329@数据,每次用户按下按钮。所以我的虚拟机看起来像这样:

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 运算符来组合两个可观察对象。 但是,在“组合来自多个发布者的元素”部分中深入研究 Publisher 的 Apple 文档,我找不到类似的东西,所以我希望目前缺少这种运算符。

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

【问题讨论】:

  • 我不知道 RxSwift,所以我不知道 withLatestFrom 做了什么。想要的功能是什么?您可能想要combineLatestzip,但这取决于您想要做什么。请参阅apeth.com/UnderstandingCombine/operators/operatorsJoiners/…如果这些都不能按预期开箱即用,请修改问题以说明它们有什么问题,我们可以添加一个修复它的运算符。
  • 不幸的是,combineLatestzip 都不能满足我的要求,因为 withLatestFrom 确保生成的流仅在第一个流(在我的示例中单击按钮)发出时才发出事件事件并从第二个流中获取最新事件。 combineLatest 在每个流的每次最新发射时发射事件,并且 zip 等待两个流发射新事件。
  • 根据this 没有等价物。
  • @sweeper 感谢您的来源,这很有帮助。我在想也许可以将现有的运算符组合起来以实现相同的行为。因为我最近经常使用withLatestFrom,所以我很确定这对很多联合用户来说会有很大的好处。
  • 好的,感谢您的澄清。不用担心!我所做的是附加另一个运算符来消除“重复”,其中重复被定义为未更改的对的有问题的成员。这样一来,只有变化才会出现。如果您要经常这样做,很容易制作一个自定义运算符来表达这样的组合。

标签: ios swift swiftui combine


【解决方案1】:

为此拥有一个内置运算符听起来很棒,但是您可以从已有的运算符中构造相同的行为,如果这是您经常做的事情,则可以很容易地使用自定义运算符现有运营商。

在这种情况下的想法是使用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($0))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}

button.map {_ in Date()}.combineLatest(text)
    .removeDuplicates {
        $0.0 == $1.0
    }
    .map {$0.1}
    .sink { print($0)}.store(in:&storage)

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

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

这似乎是你所追求的。


你会注意到我完全忽略了按钮发送的值是什么。 (在我的示例中,无论如何它只是一个 void。)如果该值很重要,则更改初始映射以将该值包含为元组的一部分,然后删除元组的 Date 部分:

button.map {value in (value:value, date:Date())}.combineLatest(text)
    .removeDuplicates {
        $0.0.date == $1.0.date
    }
    .map {($0.value, $1)}
    .map {$0.1}
    .sink { print($0)}.store(in:&storage)

这里的重点是,.map {($0.value, $1)} 行之后到达的内容与withLatestFrom 将产生的内容完全相同:两个发布者最近的值的元组。

【讨论】:

  • 这不是excat withLatestFrom rxjs.dev/api/operators/withLatestFrom。 WithLatestFrom 应该发出事件only when the source emits ---- ``` pressTheButton() typeSomeText() ``` 此代码打印字符但不应该
【解决方案2】:

作为@matt 答案的改进,这更方便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: $0, ()) }
      .prepend((value: nil, ()))

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

【讨论】:

  • 否,因为drop(untilOutputFrom 在从第二个发布者处获得值后立即取消其对第二个发布者的订阅。从而无法实现最初的目标。目标是持续监听两个发布者,并在第二个发布的任何时候从第一个发布最新值,永久。
  • 是的,已经明白了。我先改成不那么漂亮的解决方案。
【解决方案3】:

有点不回答,但你可以这样做:

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

这段代码比较明显,不用知道withLatestFrom是什么意思,虽然有捕获self的问题。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-17
    • 2016-12-12
    • 2020-03-06
    • 2023-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-31
    相关资源
    最近更新 更多