Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine: Run a list of publishers one after the other, and publish the first non-nil element

Tags:

ios

swift

combine

I have a list of combine publishers that each publish one optional value. From this list, i want to create a publisher, that runs the upstream publishers in the sequence they appear in the list, one after the other, and then publish the first non-nil item i can find.

My first approach was

publishers
    .publisher
    .flatMap(identity)
    .first(where: {$0 != nil})

but this causes all publishers to run, and the fastest to win.

I created a minimal example with a solution that comes close to what i want to achieve.

import Combine
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

func delayedPublisher<Value>(_ value: Value?, delay after: Double) -> AnyPublisher<Value?, Never> {
    let p = PassthroughSubject<Value?, Never>()
    DispatchQueue.main.asyncAfter(deadline: .now() + after) {
        p.send(value)
        p.send(completion: .finished)
    }
    return p.eraseToAnyPublisher()
}

let delays = [1,2,3,4,5].map({ Bool.random() ? nil : $0 }).shuffled()
print("Creating publishers with values and delays (in seconds)", delays)
let myPublishers = delays
    .map{ delayedPublisher($0, delay: Double($0 ?? 1))
        .print("\(String(describing: $0))")
        .eraseToAnyPublisher() }

let cancel = myPublishers
    .publisher
    .flatMap { $0 }
    .collect()
    .map { resultList in
        resultList.first(where: { $0 != nil }) ?? nil
    }
    .sink { result in
        print("result:", result ?? "nil")
    }

This creates a bunch of publishers with different delays, that may or may not produce a value. I then collect all results, and pick the first non-nil value. The problem is

  • All effects are run. I just want to run effects until i ran one that produces a value.
  • The results arrive in the order the publishers complete. I need to restore the original order after, which is possible but awkward in my specific example.

I did my research but found nothing like that, perhaps because combine is not really designed to do that sort of thing. So any pointers are appreciated.

like image 851
Falco Winkler Avatar asked Oct 22 '25 05:10

Falco Winkler


1 Answers

The approach is almost like your original one, but you need to restrict flatMap to run at most one publisher at a time with maxPublishers parameter:

publishers
    .publisher
    .flatMap(maxPublishers: .max(1), { $0 })
    .compactMap { $0 } // Remove all nil values and unwrap the non-nil ones.
    .first()
like image 130
New Dev Avatar answered Oct 23 '25 19:10

New Dev



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!