You learned in the previous episode that a publisher sends zero or more values. A publisher emits an error if something goes wrong and, if the publisher is finite, it can send a completion event. In this episode, we continue exploring the relationship between publishers and subscribers. We zoom in on the life cycle of a subscription.

Life Cycle of a Subscription

We can break the life cycle of a subscription up into five steps. Let's use the publisher we created in the previous episode as an example.

import Combine

[1, 2, 3].publisher
    .sink { value in
        print(value)
    }

Step 1: Attaching a Subscriber to a Publisher

We attach a subscriber to the publisher by calling sink(receiveValue:) on the publisher. Under the hood, the subscriber calls subscribe(_:) on the publisher. Let's revisit the Publisher protocol and take a look at the subscribe(_:) method.

func subscribe<S>(_ subject: S) -> AnyCancellable where S : Subject, Self.Failure == S.Failure, Self.Output == S.Output

The method definition confirms what we learned in the previous episode. The Output type of the publisher and the Input type of the subscriber need to match and the same is true for the Failure types of the publisher and the subscriber. We covered this in the previous episode.

The publisher's Output type is equal to Int and the publisher's Failure type is equal to Never. The sink(receiveValue:) method creates a subscriber with an Input type and a Failure type that match the Output type and the Failure type of the publisher. The only requirement of the sink(receiveValue:) method is that the publisher's Failure type is equal to Never.

Step 2: Creating a Subscription

The publisher creates a subscription in response to the subscribe(_:) call. It passes the subscription to the subscriber by calling its receive(subscription:) method. A subscription is nothing more than an object that conforms to the Subscription protocol. The Subscriber protocol confirms this. One of the required methods of the Subscriber protocol is the receive(subscription:) method.

func receive(subscription: Subscription)

The Subscription protocol inherits from the Cancellable protocol. This simply means that an object conforming to the Subscription protocol can be cancelled.

Apple's documentation includes an interesting detail that is worth pointing out. It mentions that subscriptions are class constrained. This means that a subscription is a reference type, not a value type.

Step 3: Requesting Values

The publisher creates the subscription and passes it to the subscriber. The subscriber can use the subscription it receives from the publisher to request values from the publisher. It does this by calling request(_:) on the subscription. The request(_:) method accepts a single argument of type Subscribers.Demand.

Like Publishers, Subscribers is an enum and serves as a namespace for types that act as subscribers. Demand is a struct that defines the number of values the publisher should deliver to the subscriber. It can be zero or an unlimited number of values. It is also possible to define the maximum number of values the publisher should deliver to the subscriber. We take a look at this in more detail later in this series.

func request(_ demand: Subscribers.Demand)

Step 4: Receiving Values

Once the subscriber has communicated to the publisher how many values it should deliver, the publisher can start sending values. It does this by calling receive(_:) on the subscriber, another method of the Subscriber protocol.

func receive(_ input: Self.Input) -> Subscribers.Demand

The method definition contains a few interesting details. The receive(_:) method accepts a single argument of type Self.Input, the Input type of the subscriber. It returns a Subscribers.Demand object. This is useful because it means that the subscriber can inspect the value it received from the publisher and decide whether it wants to receive more values.

Step 5: Completion

Emitting errors or a completion event are optional. Remember that some publishers don't emit errors or a completion event. Earlier in this series, we used the NotificationCenter class to create a publisher that emits UIApplication.didBecomeActiveNotification notifications. That publisher doesn't emit errors and it never completes.

Finite publishers and publishers that can emit errors call the receive(completion:) method on the subscriber to inform it that the publisher won't send any more events. The receive(completion:) method accepts a single argument of type Subscribers.Completion.

The Completion enum defines two cases, finished, if the publisher completed without errors, and failure, if the publisher completed with errors. The failure case includes the error that resulted in the completion of the publisher.

We currently use the sink(receiveValue:) method to be notified every time the publisher emits a value. The Combine framework also defines the sink(receiveCompletion:receiveValue:) method. It accepts two closures as arguments. The first closure is invoked when the publisher completes. The second closure is invoked when the publisher emits a value. Let's update the example by using the sink(receiveCompletion:receiveValue:) method.

In the first closure, we switch on the Subscribers.Completion object. Remember that the sink(receiveCompletion:receiveValue:) method can only be used on publishers that have a Failure type that is equal to Never. This implies that the print statement of the failure case won't be executed.

import Combine

[1, 2, 3].publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("finished")
        case .failure:
            print("failed")
        }
    }, receiveValue: { value in
        print(value)
    })

The output confirms that the publisher is finite. After emitting the values of the array literal one by one, it emits a completion event. This means that the publisher won't send any more events and the subscription is terminated. Any resources associated with the subscription are freed up.

1
2
3
finished

Protocol-Oriented Programming

Publisher, Subscriber, Subscription, and Cancellable are protocols. The Combine framework relies heavily on protocol-oriented programming and associated types. This opens up many possibilities as you learn later in this series.