iosswiftreactive-programmingcombine

How to make a Publisher from array of Publishers?


I am using Swift Combine and would like to make a Publisher that was created from an array of Publisher. It should emit an array of elements every time one of the Publishers in the array emits a new element.

In RxSwift would be like the following:

 let obs1 = PublishSubject<Int>()
 let obs2 = PublishSubject<Int>()
 let arrayObs = [obs1, obs2)]

 Observable.combineLatest(arrayObs)
    .subscribe(onNext: { arrayOfLatest in
        print(arrayOfLatest) //prints an array of integers
    }).disposed(by: disposeBag)

obs1.onNext(5) 
obs2.onNext(10) // prints [5,10]
obs1.onNext(12) // prints [12,10]


Solution

  • As long as you don't insist too strongly on the "array" part, combineLatest or zip is correct for the Combine framework too. The difference between them is whether you want the overall value emitted each time to contain the oldest (zip) or the newest (combineLatest) contribution from each publisher.

    Your example doesn't actually give enough information to tell which of those you want. To know which you want, you'd need to say what should happen when you say e.g.:

    obs1.onNext(5) 
    obs1.onNext(6) 
    obs2.onNext(10)
    

    Be that as it may, Combine is Swift, so it uses a tuple, not an array, to express the totality of values. But if, as in your example, all the publishers have the same output and failure types, then an array can easily be substituted by mapping. See How to zip more than 4 publishers for an example turning zip into an array processor. Exactly the same technique works for combineLatest.

    So, here's an actual working example; to make the example more general than yours, I used three publishers instead of two:

    class ViewController: UIViewController {
        let obs1 = PassthroughSubject<Int,Never>()
        let obs2 = PassthroughSubject<Int,Never>()
        let obs3 = PassthroughSubject<Int,Never>()
        var storage = Set<AnyCancellable>()
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let arr = [obs1, obs2, obs3]
            let pub = arr.dropFirst().reduce(into: AnyPublisher(arr[0].map{[$0]})) {
                res, ob in
                res = res.combineLatest(ob) {
                    i1, i2 -> [Int] in
                    return i1 + [i2]
                }.eraseToAnyPublisher()
            }
            pub.sink { print($0) }.store(in: &storage)
            obs1.send(5)
            obs2.send(10)
            obs3.send(1) // [5, 10, 1]
            obs1.send(12) // [12, 10, 1]
            obs3.send(20) // [12, 10, 20]
        }
    }