Swift Combine, как объединить издателей и поглотить, когда меняется ценность только одного издателя?

Я нашел способы объединить издателей с помощью MergeMany или CombineLatest, но, похоже, не нашел решения в моем конкретном случае.

Пример:

class Test {
    @Published var firstNameValid: Bool = false
    @Published var lastNameValid: Bool = false
    @Published var emailValid: Bool = false

    @Published var allValid: Bool = false
}

Я хочу, чтобы allValid стал false, когда любой из предыдущих издателей установлен на false, и true, если все они true. Я также не хочу жестко кодировать список издателей, за которыми я наблюдаю, поскольку мне нужно гибкое решение, поэтому я хочу иметь возможность передавать массив Bool издателей в любой код, который я использую для этого.

Я пробовал это

        let fieldPublishers = [$firstNameValid, $lastNameValid, $emailValid]
        Publishers
            .MergeMany(fieldPublishers)
            .sink { [weak self] values in
                self?.allValid = values.allSatisfy { $0 }
            }
            .store(in: &subscribers)

Но это, конечно, не работает, потому что я получаю массив издателей, а не массив значений. Я пробовал другие способы (забыл, какие из них), но они, похоже, вызывали sink только в том случае, если я присвоил значение во время выполнения всем трем издателям.

В случае использования только двух издателей мне удалось заставить его работать с помощью CombineLatest.

Итак, вопрос: могу ли я активировать sink, когда только один из издателей в массиве меняет значение после создания экземпляра Test, а затем перебирать значения всех издателей, за которыми я наблюдаю?


person Joris Mans    schedule 21.12.2020    source источник
comment
Я согласен с полученным ответом: проблема, как показано, не связана с Объединением.   -  person matt    schedule 22.12.2020


Ответы (2)


CombineLatest действительно правильный. У вас есть три издателя, поэтому вы можете использовать CombineLatest3. В этом примере я использую издателей CurrentValueSubject вместо @Published, но принцип тот же:

import UIKit
import Combine

func delay(_ delay:Double, closure:@escaping ()->()) {
    let when = DispatchTime.now() + delay
    DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
    let b1 = CurrentValueSubject<Bool,Never>(false)
    let b2 = CurrentValueSubject<Bool,Never>(false)
    let b3 = CurrentValueSubject<Bool,Never>(false)
    var storage = Set<AnyCancellable>()
    override func viewDidLoad() {
        super.viewDidLoad()

        Publishers.CombineLatest3(b1,b2,b3)
            .map { [$0.0, $0.1, $0.2] }
            .map { $0.allSatisfy {$0}}
            .sink { print($0)}
            .store(in: &self.storage)
        
        // false
        delay(1) {
            self.b1.send(true) // false
            delay(1) {
                self.b2.send(true) // false
                delay(1) {
                    self.b3.send(true) // true
                    delay(1) {
                        self.b1.send(false) // false
                        delay(1) {
                            self.b1.send(true) // true
                        }
                    }
                }
            }
        }
    }
}

Хорошо, теперь вы можете пожаловаться, это нормально для трех жестко запрограммированных издателей, но я хочу любое количество издателей. Отлично! Начните с массива ваших издателей, накапливайте их по одному с помощью CombineLatest, чтобы сформировать издателя, который создает массив Bool:

    let list = [b1, b2, b3] // any number of them can go here!
    let pub = list.dropFirst().reduce(into: AnyPublisher(list[0].map{[$0]})) {
        res, b in
        res = res.combineLatest(b) {
            i1, i2 -> [Bool] in
            return i1 + [i2]
        }.eraseToAnyPublisher()
    }

    pub
        .map { $0.allSatisfy {$0}}
        .sink { print($0)}
        .store(in: &self.storage)
person matt    schedule 22.12.2020
comment
Отличный ответ! Спасибо большое. Я превратил это в более аккуратное расширение, которое другие могут найти полезным: extension Array where Element: Publisher { func combineAll() -> AnyPublisher<[Element.Output], Element.Failure>? { guard let first = first else { return nil } return dropFirst() .reduce(into: AnyPublisher(first.map { [$0] })) { $0 = $0.combineLatest($1) { $0 + [$1] } .eraseToAnyPublisher() } } } - person Jonathan Crooke; 05.04.2021

Технически это не ответ на ваш последний вопрос, но разве свойство allValid не имеет больше смысла в качестве вычисляемого свойства?

var allValid: Bool {
   firstNameValid && lastNameValid && emailValid
}

Это гарантирует, что allValid всегда представляет собой логическое И для трех других свойств. Надеюсь, я понял суть вашего вопроса, и это помогло.

person Alex Groß    schedule 21.12.2020
comment
Вы не можете подписаться на это свойство - person Joris Mans; 22.12.2020