Search code examples
iosswiftmacoscombine

Swift Combine: prepend() before share() prevents initial sink execution


I have a setup of publishers similar as in the playground example below.

import Combine

let sub1 = PassthroughSubject<String, Never>().prepend("initial 1")//.share()
let sub2 = PassthroughSubject<String, Never>().prepend("initial 2")

Publishers.CombineLatest(sub1, sub2).sink { content1, content2 in
    print("combined received: \(content1) and \(content2)")
}

sub1.sink { content1 in
    print("first received: \(content1)")
}

sub2.sink { content2 in
    print("second received: \(content2)")
}

If the share() behind the sub1 is commented out the the following is printed by the console:

combined received: initial 1 and initial 2
first received: initial 1
second received: initial 2

This seems expected. But if the share() is present, an unexpected print is produced:

combined received: initial 1 and initial 2
second received: initial 2

In my XCode project I use prepend() to trigger an initial execution, but share() prevents the initial execution of some of my chains. Is this expected behaviour?


Solution

  • Yes, this is expected. When you use share, you basically change the publisher into reference type semantics. There is just one instance of that sub1, and as soon as you subscribe to it with sink, it publishes the first element you prepended synchronously (prepend is synchronous), and as a result the subscribers you added later don't receive the element you prepended.

    On the other hand, when you don't use share, it's all value semantics. You can think of this as sink adds a subscriber to a copy of the publisher, much like how value types are copied. Each of the copies then separately publishes their own "initial 1".

    In the documentation of share, you can see that they worked around this "share publishes all the elements synchronously to the first subscriber" problem by adding a delay:

    The following example uses a sequence publisher as a counter to publish three random numbers, generated by a map(_:) operator. It uses a share() operator to share the same random number to each of two subscribers. This example uses a delay(for:tolerance:scheduler:options:) operator only to prevent the first subscriber from exhausting the sequence publisher immediately; an asynchronous publisher wouldn’t need this.

    You can do this in your example too:

    let sub1 = PassthroughSubject<String, Never>()
        .prepend("initial 1")
        .delay(for: 0.1, scheduler: DispatchQueue.main)
        .share()
    

    And you will see the "first received" line being printed.

    Basically, the publisher now shares its elements with all its subscribers, if you want an initial element for all the subscribers, you need to figure out when exactly does all the subscribers get added (in the above case, after the two sinks), and only after that point can you publish the "initial element" (waiting 0.1 seconds is well after that). Otherwise, some subscribers do not receive that element.