'Combine bind Publisher to PassthroughSubject

This question has effectively been asked before (Combine assign publisher to PassthroughSubject) but the answer assumed the architecture of the question's example was totally wrong.

I'm faced with the same issue, in what I feel is a different example case, and I'd dearly love clarity on the issue.


I have a viewmodel class which provides a public errorMessage publisher for the client layer to do what it will with. Just a string here. Privately this publisher is backed by a PassthroughSubject - I'm "entering the monad" from different places internally and so I want to send error messages imperatively at that stage.

One of my entry points here happens to be another Combine publisher. I'd like to just map and bind, as I would in RxSwift:

private let _errorMessageSubject = PublishSubject<String>()
public let errorMessageRail = _errorMessageSubject.asObservable()

private func setup() {
   ...

   myEngine.errorMessages
     .map { msg in "Internal error: \(msg)" } 
     .bind(to: _errorMessageSubject)
   
   ...
}

private func someOtherMethod() {
   _errorMessageSubject.onNext("Surprise")
}

In Combine I'm not sure how to do it other than:

private let _errorMessageSubject = PassthroughSubject<String,Never>()
public let errorMessageRail = _errorMessageSubject.eraseToAnyPublisher()

private func setup() {
   ...

   myEngine.errorMessages
     .map { msg in "Internal error: \(msg)" } 
     .sink { [weak self] msg in
         self?._errorMessageSubject.send(msg)
     }
   
   ...
}

private func someOtherMethod() {
   _errorMessageSubject.send("Surprise")
}

Before we get into chatting concurrency issues, let's say I'm always carefully pushing to _errorMessageSubject on a specific dispatch queue. I omit that from the code above for clarity.

So given this example, unless I'm missing something staggeringly obvious a flatMap won't help me here.

  • Am I stuck with this sink -> send dance?
  • Or is there some eye-watering code-smell about my public/private publisher/subject pattern (that I use a lot for bridging imperative with reactive architectures) and can some kind soul point me in the direction of self-improvement?


Solution 1:[1]

It sounds like what you want your rail to be the merge of _errorMessageSubject and another publisher. So I'll make errorMessageRail a variable so it can be changed (by this class only) after it's initialized:

public private(set) var errorMessageRail = _errorMessageSubject.eraseToAnyPublisher()

Then, in setup, you change the rail so it includes the additional stream:

   func setup() {
        ...

        errorMessageRail = _errorMessageSubject.merge( with:
            myEngine.errorMessages
                .map { msg in "Internal error: \(msg)" }
        ).eraseToAnyPublisher()

    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Scott Thompson