Reputation: 6286
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
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
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