如何干净利落地拆分和重组 Swift 发布商值? (使用 OpenCombine)

How can I cleanly split and recombine Swift Publisher values? (using OpenCombine)

  1. 我有一个上游 Publisher 发射值。例如,十六进制颜色流。
  2. 另外,我有一个 OperatorPublisher 支持,它接受这些值并输出一些关于它们的派生值,从长远来看是一对一的,但需要有关的信息之前或之后的值。例如,return计算每种十六进制颜色与其之前颜色之间的差异。 我不能修改这个。
  3. 我想要一个简单的方法来 return 这两个值。例如。元组 (color, differenceToPreviousColor)
  4. 我使用 OpenCombine 是为了向后兼容,它可能缺少一些运算符。例如,它没有 zip(如果那是我什至需要的)

伪代码:

// Can't mess with this. Basically just returns the consecutive differences
extension Publisher {
    func colorDiff<E>() -> AnyPublisher<Color, E> where Color == Output, E == Failure {
        return self
          .map({ ([=11=], [=11=]) })
          .scan((Color.black(), Color.black()), {
            let (_, prev) = [=11=]
            let (curr, _) = 
            return (prev, curr)
          })
          .map({
            let (prev, curr) = [=11=]
            return curr - prev
          })
          .eraseToAnyPublisher()
    }
}

let colorPublisher = ... // emits #333333; #444444; ...

使用 OpenCombine(不使用 zip)执行如下操作的最简单方法是什么?

let _ = zip(colorPublisher, colorPublisher.colorDiff()).sink { (e) in } receiveValue: { (tuple) in
      debugPrint(tuple.description) // (#333333, #333333); (#444444, #111111); ...
    }

我不明白你为什么需要 zip。如果您要开始生成颜色,请捕获每种颜色并将其沿管道 连同您从中生成的颜色差异 传递下去。

这是一个使用数字的粗略模拟,但它向您展示了我的意思:

var storage = Set<AnyCancellable>()
let data = Array(1...100).shuffled()
struct Info {
    let this: Int
    let diff: Int?
    let first: Bool
}
data.publisher
    .scan (Info(this: 0, diff: 0, first: true)) { info, this in
        Info(this: this, diff: info.first ? nil : this - info.this, first: false)
    }
    .map {([=10=].this, [=10=].diff)}
    .sink { print([=10=]) }
    .store(in:&storage)

示例输出:

(80, nil)
(47, Optional(-33))
(35, Optional(-12))
...

我们最终得到一对:这个数字,以及它与前一个数字的差异(表示为可选,因为第一个数字没有前一个数字可以区分,我们需要一种表达方式)。但这些正是 zip 在您想象的场景中会给您的对,不是吗?

我想说的是,“拆分和重组”的整个概念与 Combine 的工作方式相悖。如果在管道早期出现的某些值将需要管道的后期阶段,请继续将其向下传递到中间阶段,以及中间阶段正在处理的任何其他内容。元组或像我的信息这样的载体结构对于此类工作至关重要。

可以做一个“分裂和重组”,但我无法想象它是如何工作的,除非使用zip。这是我的示例,重写为使用该方法。使用 Combine 写出来很痛苦,你会注意到我使用了 makeConnectableconnect 因为否则我示例中的所有值都会沿着一条路径溢出(因为我的示例实际上不是异步):

    let data = Array(1...100).shuffled()
    struct Info {
        let prev: Int
        let diff: Int?
        let first: Bool
    }
    let pub = data.publisher.share().makeConnectable()
    let path1 = pub
    let path2 = pub
        .scan(Info(prev: 0, diff: nil, first: true)) {
            Info(prev:, diff: [=12=].first ? nil :  - [=12=].prev, first: false)
        }
        .map {[=12=].diff}
    path1
        .zip(path2)
        .sink {print([=12=])}
        .store(in: &storage)
    _ = pub.connect()

示例输出:

(39, nil)
(56, Optional(17))
(22, Optional(-34))
...

一个“hacky”解决方法是通过 handleEvents(OpenCombine 有这个),它允许您拦截在发布者的生命周期内发生的不同事件。

// just a placeholder, giving an initial value to avoid the need for
// an optional, anyway this value will be overwritten by the first
// published value
var currentColor = Color.black()

_ = colorPublisher
    // intercept the published value, stored it into a local variable
    .handleEvents(receiveOutput: { currentColor = [=10=] })
    .colorDiff()
    // now combine the latest received color and the diff, into a tuple
    .map { (currentColor, [=10=]) }
    .sink(receiveValue: { debugPrint([=10=]) })

或者,如果您想捕获最后两项(对于 3,4... 项,您可以扩展元组或使用数组):

var latestColors = (secondLast: Color.black(), last: Color.black())
_ = colorPublisher
    .handleEvents(receiveOutput: { latestColors = (latestColors.last, [=11=]) })
    .colorDiff()
    .map { (latestColors.last, latestColors.secondLast, [=11=]) }
    .sink(receiveValue: { debugPrint([=11=]) })

关于 zip 运算符的可用性,OpenCombine 对此有一个 PR - https://github.com/OpenCombine/OpenCombine/pull/109,但该 PR 尚不可合并,但如果您想查看,可以使用该分支运算符实现。