Swift Combine:如何创建可重复使用的 Publishers.Map 以连接到多个上游发布者?

Swift Combine: How can I create a reusable Publishers.Map to connect to multiple upstream Publishers?

我正试图全神贯注于 Combine,当我重构代码时,当我尝试重新组合以避免重复自己时,我 运行 陷入了一些混乱。

在这种情况下,我有一个值我想随时更新:

由于 3 秒刷新计时器在第一次触发之前不会发布任何内容,因此我假设我需要多个发布者。

我总是只使用来自主题的值,而忽略从前台通知和计时器发送的任何值。

这是一些示例代码,我在其中处理仅基于一个发布者的值:

import UIKit
import Combine

class DataStore {

    @Published var fillPercent: CGFloat = 0

    private var cancellables: [AnyCancellable] = []

    // how much the glass can hold, in milliliters
    private let glassCapacity: Double = 500

    // how full the glass is, in milliliters
    private var glassFillLevelSubject = CurrentValueSubject<Double,Never>(250)

    // a publisher that fires every three seconds
    private let threeSecondTimer = Timer
        .publish(every: 3,
                 on: RunLoop.main,
                 in: .common)
        .autoconnect()

    // a publisher that fires every time the app enters the foreground
    private let willEnterForegroundPublisher = NotificationCenter.default
        .publisher(for: UIApplication.willEnterForegroundNotification)

    init() {
        // publisher that fires any time the glass level changes or three second timer fires
        let glassLevelOrTimerPublisher = Publishers.CombineLatest(glassFillLevelSubject, threeSecondTimer)
            // is there shorthand to only return the first item? like .map{ [=12=] }?
            .map { glassFillLevel, timer -> Double in
                return glassFillLevel
            }
            .eraseToAnyPublisher()

        // publisher that fires any time the glass level changes or three second timer fires
        let glassLevelOrForegroundPublisher = Publishers.CombineLatest(glassFillLevelSubject, willEnterForegroundPublisher)
            .map{ glassFillLevel, notification -> Double in
                return glassFillLevel
            }
            .eraseToAnyPublisher()

        // how can I define map and everything after it as something, and then subscribe it to the two publishers above?
        glassLevelOrTimerPublisher
            .map{ fillLevelInMilliliters in

                let fillPercent = fillLevelInMilliliters / self.glassCapacity

                return CGFloat(fillPercent)
            }
            .assign(to: \.fillPercent, on: self)
            .store(in: &cancellables)
    }
}

我想我想在这里做的是以某种方式分离出 .map 和它后面的所有内容,并以某种方式将其订阅给上面的两个发布者。

我试过这个,作为隔离 .map 之后的所有内容以使其可重用的方法:

    let fillPercentStream = Publishers.Map{ fillLevelInMilliliters in

        let fillPercent = fillLevelInMilliliters / self.glassCapacity

            return CGFloat(fillPercent)
        }
        .assign(to: \.fillPercent, on: self)
        .store(in: &cancellables)

但这给了我一个错误 Missing argument for parameter 'upstream' in call 所以我尝试为该参数添加一些东西,结果是这样的:

    let fillPercentStream = Publishers.Map(upstream: AnyPublisher<Double,Never>, transform: { fillLevelInMilliliters in

            let fillPercent = fillLevelInMilliliters / self.glassCapacity

            return CGFloat(fillPercent)
        })
        .assign(to: \.fillPercent, on: self)
        .store(in: &cancellables)

然后,我遇到了一系列编译器错误:Unable to infer complex closure return type; add explicit type to disambiguate 它建议我在 .map 中指定 -> CGFloat,我添加了它,但后来它告诉我应该将 CGFloat 更改为 _,我最终会遇到更多错误。

这甚至是我应该能够用 Combine 做的事情吗?我会以错误的方式处理这件事吗?我如何正确地重新使用两个不同发布者的 .map.assign 链?

总的来说,我对 Combine 和响应式编程有些陌生,所以如果您有其他建议来改进我在这里做的所有事情,请告诉我。

处理此问题的一种方法是将您的计时器发布者输出映射到主题的值,并将您的通知发布者输出映射到主题的值。然后,您可以将主题、计时器和通知合并到一个发布者中,该发布者在主题的值更改时、计时器触发时以及通知发布时发布主题的值。

import Combine
import Foundation
import UIKit

class DataStore: ObservableObject {
    init() {
        fractionFilled = CGFloat(fillSubject.value / capacity)

        let fillSubject = self.fillSubject // avoid retain cycles in map closures
        let timer = Timer.publish(every: 3, on: .main, in: .common)
            .autoconnect()
            .map { _ in fillSubject.value }
        let foreground = NotificationCenter.default
            .publisher(for: UIApplication.willEnterForegroundNotification)
            .map { _ in fillSubject.value }
        let combo = fillSubject.merge(with: timer, foreground)
        combo
            .map { [capacity] in CGFloat([=10=] / capacity) }
            .sink { [weak self] in self?.fractionFilled = [=10=] }
            .store(in: &tickets)
    }

    let capacity: Double = 500
    @Published var fractionFilled: CGFloat

    private let fillSubject = CurrentValueSubject<Double, Never>(250)
    private var tickets: [AnyCancellable] = []
}