This is Understanding Combine, written by Matt Neuburg. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current (and final) editions are iOS 15 Programming Fundamentals with Swift and Programming iOS 14. Thank you!


Multicast

.multicast accepts one parameter: either a Subject or a function that produces a Subject. The operator object produced by this operator (Publishers.Multicast) is a class, not a struct, and it adopts the ConnectablePublisher protocol, which means that it has a connect method and an autoconnect method, just like a Timer (which also adopts ConnectablePublisher).

Internally, this operator maintains the Subject, and when it receives a value from upstream, it calls send on the Subject, handing it that value. So the Subject merely echoes downstream the value that this operator receives from upstream. But we already know that a Subject behaves as a splitter! So this operator, by being a class, which has “reference semantics”, and by maintaining a Subject internally, is itself functioning as a splitter.

In fact, that is how .share works under the hood: internally, a Share object is a Multicast object! .share is just a convenient wrapper for .multicast. But it is convenient, which makes it less likely that you would need to use .multicast explicitly. So why would you use .multicast explicitly? Well, the .multicast inside .share is followed by .autoconnect, so the pipeline starts going as soon as it has a subscriber. If you want the power to set the pipeline going manually by calling connect yourself, you can use .multicast. You might also use .multicast if you want more control over the type of Subject that is being dispensed.

To illustrate, I’ll turn a Timer into a multicasting Timer and store it as an instance property:

let myMulticastingTimer = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .scan(0) {i,_ in i+1}
    .multicast(subject: PassthroughSubject())

Now, myMulticastingTimer is a Connectable; and since we have not attached the .autoconnect() operator to it, the pipeline won’t start until it is told explicitly to connect.

So what happens if a subscriber comes along?

self.myMulticastingTimer
    .sink(receiveCompletion: { print($0)}, receiveValue: { print($0)})
    .store(in:&self.storage)

Of itself, nothing happens: the Timer doesn’t start running, and the subscriber doesn’t receive any values from upstream. The subscription is established; but that’s all. So what would need to happen for the upstream pipeline to start emitting values? We’d need to tell it to connect, and retain the resulting Cancellable object to prevent it from cancelling immediately:

self.myMulticastingTimer.connect()
    .store(in:&self.storage)

As soon as we say that, the upstream pipeline comes to life, and the Timer starts sending values (beginning at 1) to any connected subscribers. Moreover, the pipeline is a multicasting pipeline, so if there are multiple subscribers, they will be receiving the same value simultaneously.

Each subscribing pipeline can fail separately in good order without any effect on the other subscribing pipelines. However, there is also no effect on the upstream pipeline; no cancel message from below the .multicast operator is passed upward. The only way to cancel the upstream pipeline, stopping the Timer, is through the Cancellable object that we received by calling connect in the first place.


Table of Contents