Apple’s Combine framework has two operators called .share() and .makeConnectable(). They are extremely useful but often misunderstood. Both help us avoid unnecessary processing. .share(), as the name suggests, helps us share a subscription. .makeConnectable() helps control when publishers emit events.

Working with Multiple Subscribers

Imagine you have a simple view with a button and two labels. When a user taps the button, the text on the two labels will be updated.

The UI might look like this.

a button and two labels

Next, we make a Combine data stream that takes a tap on the button, creates a string and then updates both labels.

let didTap = didTapPublisher
    .map { _ -> String? in "Updated Label" }

didTap
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

didTap
    .assign(to: \.labelTwo.text, on: self)
    .store(in: &cancellables)

Tapping on the button updates both labels.

a button and two updated labels

In this example, the didTapPublisher is actually a PassThroughSubject linked to the UIButton tap event. The PassThroughSubject is a class and as such acts as a single publisher.

But when we assign the output of didTap to each label, we actually create two subscription that we then store in our cancellables.

two subscribers to one publisher

That makes a lot of sense. The didTapPublisher acts as the single source of truth for a tap event and pushes the mapped strings to the labels.

An API Call with Multiple Subscribers

Now we will update our code so the tap makes an API call. The API call will fetch a random user ID every time. When the fetch response is received we will update the UI with the received ID. In the following example, .flatMap { _ in API.getUserID() } does the networking logic so we can focus on our subscriptions.

let didTap = didTapPublisher
    .flatMap { _ in API.getUserID() }

didTap
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

didTap
    .assign(to: \.labelTwo.text, on: self)
    .store(in: &cancellables)

Now when we tap the button our updated UI might look like this.

after fetch user ids do not match

Notice that a different ID is displayed for each label. This is because each subscription is causing a separate API call. It looks a bit like this.

one publisher but multiple calls

Although one publisher is emitting the tap event, there are two subscriptions. The API call is part of the subscription. So we end up with two API calls.

It’s easy to imagine a more complex UI with multiple subscriptions causing multiple network calls. This is what we want to avoid. So we need .share().

Share the Upstream

In our next example, we want to share the events from the upstream publisher. We don’t want to cause multiple API calls as this is resource intensive and we also want to ensure the UI gets the same output.

This is where we need the .share() operator.

Apple states that:

Share is a class instance that shares elements received from its upstream to multiple subscribers.

So .share() specifically exists to share the events from the upstream publisher.

Note that .share() returns a new class of type Publishers.Share. If you are familiar with reference semantics then it makes sense as we only want to refer to one shared instance of the upstream publishers.

Let’s add it to our code.

let didTap = didTapPublisher
    .flatMap { _ in API.getUserID() }
    .share() // <-- added share

didTap
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

didTap
    .assign(to: \.labelTwo.text, on: self)
    .store(in: &cancellables)

Now the UI is what we would expect.

one API call and matching ids

The data streams look more like this.

diagram of stream being shared

In these examples, the API Call is triggered by a tap on the button. The data stream is a push-style data stream, where events are pushed to subscribers from the upstream. But what if events are triggered by subscriptions? What if subscriptions pull data from an upstream publisher?

What if Downstream Subscriptions Pull the Events?

We can change our previous code so that instead of API calls being triggered by a tap, they are triggered by a subscription. Here’s how that looks.

let apiCall = API.getUserID()
    .share()

apiCall
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

apiCall
    .assign(to: \.labelTwo.text, on: self)
    .store(in: &cancellables)

Now, the first subscription that assigns the output from the API call to labelOne.text is actually what triggers the network call. This means that the second subscription must attach itself to the publisher before the API response is received, otherwise, it might miss the event.

In this very small example, this is virtually never going to happen as the API call will often take longer than the subsequent subscription. But if we delay the subscription we can see the result. Let’s try it.

let apiCall = API.getUserID()
    .share()

apiCall
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [self] in
    apiCall
        .assign(to: \.labelTwo.text, on: self)
        .store(in: &cancellables)
}

That breaks it.

missed the network call

How can we ensure that all subscribers are subscribed before they apply pressure to the upstream publisher and pull events?

ConnectablePublisher and makeConnectable

This is exactly what the operator makeConnectable() is for. It is operator that returns a ConnectablePublisher.

This publisher doesn’t produce any elements until you call its connect() method.

The connect() method becomes the trigger for pushing events. This gives us time to ensure all our subscribers are connected before we start sending events.

Using this we can update our code:

let apiCall = API.getUserID()
    .share()
    .makeConnectable()

apiCall
    .assign(to: \.labelOne.text, on: self)
    .store(in: &cancellables)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [self] in
    apiCall
        .assign(to: \.labelTwo.text, on: self)
        .store(in: &cancellables)
    
    apiCall.connect().store(in: &cancellables)
}

Now both labels will be updated with the same response sometime after the second subscriber is subscribed.

one API call and matching ids

Simple.

The API Code

If you wanted to replicate this post feel free to use this API code to get going.

struct User: Codable {
    let id: Int
}

enum API {
    static let users = URL(string: "https://random-data-api.com/api/v2/users")!
    static func getUserID(url: URL = API.users) -> AnyPublisher<String?, Never> {
        URLSession.shared
            .dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .map { user in "Received User ID: \(user.id)"}
            .replaceError(with: "error")
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }
}

Conclusion

The .share() and .makeConnectable() operators in the Combine framework are powerful tools for managing the flow of data in your Swift applications. .share() allows multiple subscribers to receive updates from a single publisher, while .makeConnectable() allows for manual control over when data is emitted from a publisher. Both operators can be useful in a variety of situations.

Bonne chance.