Combine: Sharing Publishers

Combine: Sharing Publishers

With this short reference code snippet, you will be able to share publishers and avoid multiple asynchronous downloads of the same data.

This brief overview will demonstrate some basic features that may come in handy when working with publishers in Combine, Apple's framework to handle asynchronous events by combining event-processing operators. The Publisher protocol declares a type that transmits a sequence of values over time that subscribers can receive as input by adopting the Subscriber protocol.

A typical use case for Combine would be handling asynchronous download of data from the internet using a URLSession dataTask. In a scenario like that, it can also be smart to share a publisher to avoid multiple downloads of the same data.

This can be achieved by using the new URLSession.shared.dataTaskPublisher(for: URL) with its convenient .share() operator to share elements received from its upstream to multiple subscribers. For example in a Swift Playground:

import Foundation
import Combine

guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
    fatalError("Invalid URL")
}

let request = URLSession.shared.dataTaskPublisher(for: url).map(\.data).print().share()

let subscription1 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 1")
    print($0) //27520 bytes
})

let subscription2 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 2")
    print($0) //27520 bytes
})

By adding the .print() operator to the dataTaskPublisher, the process can be followed on the console. Since subscription1 and subscription2 both subscribe to the same publisher, they both receive the value, but it is only downloaded one time.

receive subscription: (DataTaskPublisher)
request unlimited
receive value: (27520 bytes)
Subscription 1
27520 bytes
Subscription 2
27520 bytes
receive finished

However, this process only works until the publisher has published the data and finished the dataTask. In the example, subscription1 and subscription2 are executed in immediate succession, allowing the process to work as intended since the download has not yet completed.

If you add another subscription3 and delay its execution long enough for the dataTask to be completed before the subscription is added, subscription3 will not receive any value since the publisher has already published his data before subscription3 actually happens.

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    let subscription3 = request.sink(receiveCompletion: { _ in }, receiveValue: {
        print("Subscription 3")
        print($0)
    })
}

Where to go next?

If you are interested in knowing more about working with Combine and the various operators available to shape the sequence of values send by publishers and received by subscribers, check our other articles on Combine that will be released over the next days and weeks.

They will cover how to work with filters, sequence, and transformation operators or timers, how to use the dataTaskPublisher, how to combine publishers as well as how to debug Combine messages, and much more.