Falco Winkler
Falco Winkler

Reputation: 1190

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

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

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.

Upvotes: 3

Views: 3537

Answers (2)

Stormwright
Stormwright

Reputation: 172

Here's a simple extension to Publisher

extension Publisher {
  func chain(with publisher: @escaping () -> AnyPublisher<Output, Failure>) -> AnyPublisher<Output, Failure> {
    self.flatMap { _ in publisher() }.eraseToAnyPublisher()
  } 
}

Upvotes: 1

New Dev
New Dev

Reputation: 49590

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()

Upvotes: 6

Related Questions