首页 > 解决方案 > Swift Combine:如何创建可重用的 Publishers.Map 以连接到多个上游 Publisher?

问题描述

我正试图将我的头脑围绕在 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{ $0 }?
            .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它建议我指定-> CGFloatin .map,我添加了它,但随后它告诉我应该更改CGFloat_并最终出现更多错误。

这甚至是我应该能够用Combine做的事情吗?我是不是走错了路?如何正确地与两个不同的发布者重复使用.mapand.assign链?

一般来说,我对组合和反应式编程有点陌生,所以如果您有其他建议来改进我在这里做所有事情的方式,请告诉我。

标签: ioscombine

解决方案


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

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($0 / capacity) }
            .sink { [weak self] in self?.fractionFilled = $0 }
            .store(in: &tickets)
    }

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

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

推荐阅读