DEV Community

Stoian Dan
Stoian Dan

Posted on • Edited on

How Combine works: Publishers

Apple's Combine.framework provides support for async and reactive programming, and while Swift's latest async/await keywords make it easy to write code that reads linear, but is asynchronous, that's not a 100% overlap with Combine, hence, it's still relevant.

Combine is not all about async programming, but about reactive programming, about streaming data and reacting to change in data.

Basically, reactive programming is about reacting to changes in data, or to events. This is done via streams; channels that unite our data during all time and update us with any possible changes as they come.

Therefore, talking the above concepts into consideration, let's see how the map to the Combine API.

Combine defines:

  • Publisher - a protocol to which one ca subscribe to receive values over time
  • Subscriber - a client of a Publisher, that receives the values
  • Subscription the binding between a publisher and subscriber
  • Finally, there is the concept of an operator. Operators are just Publishers that act as intermediary between a upstream publisher and a downstream publishers or a terminal subscriber. They often transform/prepare data for the end subscriber. This allows for greater modularity as these operators can be reused, inserted or removed at will, thus creating an effective pipeline between the initial, upstream publisher and the end subscriber.

Let's say we want to create a Publisher that publishes all values from 0 to n, like a range, starting always from 0 and going until n. We'll call it Until.

A publisher doesn't necessarily directly create its own values. Publishers are often structs, i.e. value types, they can be created in place and aren't pass around. There is notable exception that is the Subject publisher.

The Publisher protocol is generic over two types, an Output, the type of data which it will yield, and a Failure, the type of error which it yields, should something go wrong:

protocol Publisher<Output, Failure>
Enter fullscreen mode Exit fullscreen mode

Until will always yield Int values and there's no real good reason to encounter and error, so our Failure type will be Never.

Other than that the only required method a Publisher needs implement is called receive that receives a Subscriber, acknowledging it by providing back a subscription.

func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
Enter fullscreen mode Exit fullscreen mode

This might look complicated, all it's really saying is that the method is generic every a subscriber type and that subscribes's expected input value type needs match our expect output type, so with the failure type, makes sense?

Let's implement this:

import Foundation
import Combine

struct Until: Publisher {
    // we output integers
    public typealias Output = Int
    // we never throw errors
    public typealias Failure = Never

    // the end value at which stop
    let endValue: Int


    public init(_ endValue: Int) {
        self.endValue = endValue
    }

    public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Int == S.Input {
      // TODO: implement it
    }
}
Enter fullscreen mode Exit fullscreen mode

Great! Now how do we create a subscription to give it back to our subscriber? I'll leave that for a next episode.
For now out publisher will be a bit violent. Normally, the subscriber receives values only after he initially demands them. After each value received, he can demand even more, or less, as he wishes.

For now, our publisher will generate his own values and send them to the subscriber, regardless if he wants or not, but in the next episode we will do this right:

    public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Int == S.Input {
        // a dummy subscription that ignores demands
        let subscription = Subscriptions.empty
        subscriber.receive(subscription: subscription)

        // send the subscriber the value immediately
        // ignoring any of his possible demands
        for val in 0...endValue {
           let _ = subscriber.receive(val)
        }
        // tell our subscriber we are done
        subscriber.receive(completion: .finished)

    }
Enter fullscreen mode Exit fullscreen mode

How do we use this? We will need a subscriber, lucky Apple made one called Sink, it automatically demands an unlimited amount of data and has a callback where we can use the data provided by the publisher:

// store the subscription provided by sync, 
// normally if we would not do this, it will cancel our request
var cancellables = Set<AnyCancellable>()

    Until(10).sink(receiveValue: { val in
        print("We received \(val) from publisher!")

    }).store(in: &cancellables)

Enter fullscreen mode Exit fullscreen mode

Top comments (0)