Swift Combine: combining three signals into one

Viewed 654

I'm dealing with a legacy libraries where I'm not at liberty to modify their code, and am trying to use Combine to weave them into something more easy to use. My situation is that a method call can either return a response, or a response and two notifications. The response-only is a success scenario, the response + 2 notifications is an error scenario. I want to combine both response and payload from the two notifications into an error that I can pass on to my app. The really fun thing is that I don't have a guarantee if the response or notifications come first, nor which of the notifications comes first. The notifications come in on a different thread than the response. The good thing is that they come in "just about the same time".

For handling a notification, I do

firstNotificationSink = notificationCenter.publisher(for: .firstErrorPart, object: nil)
  .sink { [weak self] notification in
    // parse and get information about the error
  }

secondNotificationSink = notificationCenter.publisher(for: .secondErrorPart, object: nil)
  .sink { [weak self] notification in
    // parse and get more information about the error
  }

and asking the legacy library for a response is:

func doJob() -> String {
  let resultString = libDoStuff(reference)
}

Is there a way for me to use Combine to merge these three signals into one, given i.e. a 50ms timeframe? Meaning, if I get the result and two notifications, I have an error response I can pass on to my app, and if I have only the result and no notifications arrived in 50ms, then I can pass that success response to my app?

1 Answers

The part about combining the three signals is easy: use .zip. That's not very interesting. The interesting part of the problem is that you want a pipeline that signals whether a notification arrived within a certain time limit. Here's an example of how to do that (I'm not using your actual numbers, it's just a demo):

import UIKit
import Combine

enum Ooops : Error { case oops }

class ViewController: UIViewController {
    var storage = Set<AnyCancellable>()
    override func viewDidLoad() {
        super.viewDidLoad()
        print("start")
        NotificationCenter.default.publisher(for: Notification.Name("yoho"))
            .map {_ in true}
            .setFailureType(to: Ooops.self)
            .timeout(0.5, scheduler: DispatchQueue.main) { Ooops.oops }
            .replaceError(with: false)
            .sink {print($0)}
            .store(in: &self.storage)
        DispatchQueue.main.asyncAfter(deadline:.now()+0.2) {
            NotificationCenter.default.post(name: Notification.Name("yoho"), object: self)
        }
    }
}

If the asyncAfter delay is 0.2, we get true (followed by false, but that's not important; we could change that if we wanted to). If the delay is 0.9, we get false. So the point is, the first value we get distinguishes correctly whether we got a signal in the required time.

Okay, so the rest is trivial: you just hook up your three signals with .zip, as I said before. It emits a tuple after all three publishers have emitted their first signal — and that's all the information you need, because you've got the result from the method call plus Bools that tell you whether the notifications arrived within the time limit. You can now read that tuple and analyze it, and do whatever you like. The .zip operator has a map function so you can emit the result of your analysis in good order. (If you wanted to transform the result of the map function into an error, that would require a further operator, but again, that's easy.)

Related