Joris
Joris

Reputation: 6286

Swift Combine, how to combine publishers and sink when only one publisher's value changes?

I have found ways of combining publishers using MergeMany or CombineLatest, but I don't seem to find a solution in my particular case.

Example:

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

    @Published var allValid: Bool = false
}

I want allValid to become false when any of the previous publishers are set to false, and true if all of them are true. I also don't want to hardcode the list of publishers I am observing since I want a flexible solution, so I want to be able to pass an array of Bool publishers to whatever code I use to do this.

I tried this

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

But this of course doesn't work because I get an array of publishers and not an array of values. I tried some other ways (forgot which ones) but they only seemed to call sink if I assigned a value during execution to all 3 publishers.

In the case of only using 2 publishers I managed to get it working using CombineLatest.

So the question is: Can I have sink triggered when only one of the publishers in an array changes value after instantiation of Test, and then iterate over the values of all the publishers I am observing?

Upvotes: 5

Views: 10124

Answers (2)

matt
matt

Reputation: 534893

CombineLatest is indeed correct. You have three publishers so you would use CombineLatest3. In this example, I use CurrentValueSubject publishers instead of @Published, but it's the same principle:

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
                        }
                    }
                }
            }
        }
    }
}

Okay, now you may complain, that's okay for a hard-coded three publishers, but I want any number of publishers. Fine! Start with an array of your publishers, accumulate them one at a time with CombineLatest to form a publisher that produces an array of 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)

Upvotes: 10

Alex Gro&#223;
Alex Gro&#223;

Reputation: 22

This is technically not an answer to your last question but wouldn’t the property allValid make more sense as a computed property?

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

This would make sure, that allValid at all times represents the logical AND for the other three properties. I hope that I have understood the core of your question and this helped.

Upvotes: -2

Related Questions