How to make a Publisher that sends multiple values in Combine

Viewed 832

Could you help me with a Combine issue?

I'd like to make a Publisher which sends multiple values during its lifetime. In more detail, I want to wrap a method with a completion handler into a Publisher, and the completion handler is supposed to be called multiple times. For example, it's a method used for receiving messages via WebSocket like this:

webSocketClient.receiveMessage { message, error in
    // this closure is called multiple times, everytime a message comes
    ...
}

How can I wrap this into a Publisher? I want something like AnyPublisher<String, Error> in the end.

Wrapping these is super easy when I use other FRP libraries. For example, in ReactiveSwift, it can be achieved by using SignalProducer.init(_ startHandler:). In RxSwift, it's Observable.create method.

In Combine, I found Future can be used when there's only one value to emit, but it doesn't suit my current case. I couldn't find any initializer for Publisher for this use case.

Also, I found Effect.run in TCA (The Composable Architecture) which can be used for this case. It seems custom publishers are used in its implementation, which seems a bit complicated for simple usage, but is this the only way? Are there any other easy ways to achieve the similar behavior?

I feel this is a quite common scenario, so I'd like to know how Combine users are handling this case in practice.

Thank you in advance!

1 Answers

Basically there are two options.

  • In a SwiftUI environment make the class which contains the code conform to ObservableObject and add a @Published property

    class ViewModel : ObservableObject {
          @Published var message = ""
    
          func loadData() {
              webSocketClient.receiveMessage { message, error in
                  self.message = message
                  ...
              }
          }
      }
    

    In the view create a @StateObject of the view model to be notified about new messages.

  • Or declare a subject to send values

    class ViewModel {
          let subject = PassthroughSubject<String,Never>()
    
          func loadData() {
              webSocketClient.receiveMessage { message, error in
                  self.subject.send(message)
                  ...
              }
          }
      }
    

    To receive the notifications get an instance of the class, call sink on the subject and store the result into a strong reference.

Related