How to Create Custom Publishers in Combine

If you really need them

Combined Tutorials
Better Programming
Published in
6 min readMay 7, 2020

--

Photo by Jason Leung on Unsplash

As you’ve seen during the Combine learning path, publishers and subscribers cooperate with each other creating a powerful tool to exchange information inside your application.

Maybe the publishers provided by the framework will be enough for your work, but what about custom publishers? Is it possible to create custom ones and, if yes, how to do it?

Of course, it is, and it’s not really complex as it may seem at first. First of all, in order to create a working stream, we need three things:

  1. Publisher
  2. Subscription
  3. Subscriber

Using Combine, you see mostly publishers and subscribers — but the real magic behind the communication is made possible by subscription.

In fact, the publisher creates a subscription with all the required data and gives it to the subscriber.

Once the subscriber receives the subscription and real communication between the entities are created, the subscription starts its engine processing values over time.

The Subscription Mechanism

Let’s see how the three-component works and interacts with each other. This will simplify our understanding of custom publishers because we’ll have clear what is needed in order to make the asynchronous communication work.

  1. A subscriber subscribes to the publisher. This is usually done via the sink() method, which allows the programmer to manage both the completion and the values received by the publisher.
  2. The publisher creates a subscription and delivers it to the subscriber. The subscriber receives the subscription via the receive(subscription:) method.
  3. Now that the subscriber has the subscription and a contract between it and the publisher has been made, it can request for values to the publisher. This is done by sending the number of values it wants, by calling request(_:).
  4. The subscription receives the demand and starts to process information and emit values. Those values are sent one by one using the subscriber’s receive(_:) method until the maximum demand is reached.
  5. The subscriber receives values from the subscription, and, as a response, it sends a new Demand. Note that the Demand that is sent adds the number of values it wants to receive upon the previous demand already sent as the number of values requested can increase or stay the same but never decrease.
  6. Once the Demand is received by the Subscription, it starts the whole processing again and emits values until the Demand is satisfied. Steps 4, 5, and 6 are repeated indefinitely.

As you may have noticed, the entire communication is pretty easy, and once understood, implementing custom publishers is a doable task.

Sometimes, implementing the Publisher protocol itself is not necessary and the same goal can be achieved in other ways:

  1. You can use a PassthroughSubject, for example, a subclass of Subject, to publish values by calling its send(_:) method
  2. Use a CurrentValueSubject to publish whenever you update the subject’s value. The CurrentValueSubject, in fact, is a subject that wraps a single value and publishes a new element whenever the value changes.
  3. Use the @Published annotation on a property so that the property gains a publisher that emits an event whenever the property’s value changes. This approach is used a lot in SwiftUI.

In case you need a custom publisher because it fits your needs, the easiest way is to create an extension to the Publisher namespace.

Let’s give it a try.

Imagine you want to create a new operator because the standard ones do not implement the functionality you need. To give a more concrete example, imagine you want to recreate an operator from RxSwift, another declarative framework for Swift that you may know.

What the operator does is: take two publishers, combines them, removes the duplicates, and emit the outputs.

The most complex thing is the method signature, but don’t worry, it’s easier than you may think. First of all, it returns an AnyPublisher with the same output and Failure type of the current publisher you’re working on.

Tip: Using AnyPublisher as a return type is a good choice because when chaining operators, it becomes really easy to make the signature complicated.

Next, because the output from both publishers has to be compared, the Output has to adopt the Equatable protocol. Notice how the generic use is applied. We want to make it generic; otherwise, the operator loses its versatility to work with any kind of Publisher.

The body of the function is trivial: we combine the publisher with another one passed as a parameter and then call two other basic operators: removeDuplicates and map. eraseToAnyPublisher(_:) method allows the operator to be converted into an AnyPublisher as previously recommended.

Have you seen how easy it is? And what about a brand new publisher?

Well, to make the example more clear, if you’re familiar with the dataTaskPublisher() from the URLSession class, we’ll try to recreate the same behaviour making a new publisher and a new subscription.

Please notice that you can create a new publisher and NOT a new subscription, but problems may arise because you lose the ability to cope with subscriber demands, and this makes the implementation tricky for the Combine ecosystem.

Subscription

Let’s start by creating our Subscription, extending the Publishers enum.

As we have seen in the subscription mechanism, the Subscription object is passed from Publisher to Subscriber and so on. This scenario makes the class, instead of a struct, more suitable to our needs, because we want to pass it by reference instead of making copies.

In the class signature, we specify the Input type which has to be Data and the failure to be Error, accordingly to what’s returned by the standard URLSession methods.

We need a URLRequest to work on (we could have also used a URL and make a different implementation) and a subscriber to which we pass data and errors received.

Both properties are initialised via a constructor. Of course, the subscriber has to be an Optional, because, as you may imagine, it can be attached to the Subscription or not, depending on the current state of the program.

In fact, we also need the cancel method that puts the subscriber at nil.

In sendRequest, magic happens. We call the normal dataTask method on the URLSession object and bind it to the subscriber using the map function.

Publisher

Now it’s time to create a publisher. Publishers are basically a structure.

After we’ve adopted the Publisher protocol, we have to define both the Output and the Failure, in our case Data and Error.

A custom initializer is used to instantiate the URLRequest object that carries information on which server to contact and the configuration.

The receive(subscriber:) method is the core of the Publisher. First, in the signature, we have to make a check for the Failure and Output type, to make sure that the Subscriber fits into the Publisher types. A new subscription is then created and passed to the subscriber, making the subscription mechanism work!

Last but not least, to make sure the subscriber is able to receive values from the DataPublisher, we should make a method that returns the DataPublisher so we can apply, for example, the sink() method and perform the operation on data emitted by the publisher.

Its usage can be something like this.

Now that you’ve understood how the subscription mechanism work and how to implement a custom publisher, you’re free to test it with other components from the iOS ecosystem!

To see all the code used in this article, visit the following page:

CustomOperator.playground

CustomPublisher.playground

If you want to explore other topics, visit our repository on GitHub:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

What are your thoughts?