Swift Combine: Как я могу создать многократно используемый Publishers.Map для подключения к нескольким вышестоящим издателям? - PullRequest
0 голосов
/ 28 апреля 2020

Я пытаюсь обернуть голову вокруг Combine, и при рефакторинге кода у меня возникает путаница, когда я пытаюсь пересоздать, чтобы избежать повторения.

В этом случае у меня есть значение, которое я хочу обновить в любое время:

  • Спецификация c изменения предмета
  • Приложение выходит на передний план
  • 3-секундная ссылка sh таймер срабатывает

Так как 3-секундный таймер refre sh не публикует sh ничего до его первого запуска, я предполагаю, что мне нужно несколько издателей.

Я всегда использую только значение из темы и игнорирую любые значения, отправленные из уведомлений и таймера переднего плана.

Вот пример кода, в котором я обрабатываю значение, основанное только на одном издателе:

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 и предлагает указать -> CGFloat в .map, который я добавил, но затем он говорит мне, что я должен изменить CGFloat на _, и я получаю больше ошибок .

Это то, что я должен был делать с Комбинатом? Я иду по этому поводу все неправильно? Как я могу правильно использовать цепочку .map и .assign с двумя разными издателями?

Я немного новичок в области комбинированного и реактивного программирования в целом, так что если у вас есть другие предложения по улучшению того, как я Я делаю все здесь, пожалуйста, скажите мне.

1 Ответ

1 голос
/ 29 апреля 2020

Один из способов справиться с этим - сопоставить выходные данные издателя таймера со значением субъекта, а также сопоставить выходные данные издателя уведомлений со значением субъекта. Затем вы можете объединить тему, таймер и уведомление в одном издателе, который публикует значение субъекта, когда значение субъекта изменяется и когда срабатывает таймер и когда публикуется уведомление.

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] = []
}
...