This is Understanding Combine, written by Matt Neuburg. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current (and final) editions are iOS 15 Programming Fundamentals with Swift and Programming iOS 14. Thank you!


Sink

The .sink subscriber (Subscribers.Sink) is sort of an all-purpose subscriber. In its fullest form, it is created with this method:

func sink(receiveCompletion: f1, receiveValue: f2) -> AnyCancellable

(I’ll deal later with the significance of the fact that the sink method produces an object typed as AnyCancellable rather than Subscribers.Sink.)

In my notation, f1 and f2 are functions. They will usually be anonymous functions, and the second function will usually be written using trailing closure syntax, so the receiveValue label will be omitted. A typical call will therefore look more or less like this:

.sink(receiveCompletion: {
    comp in // ...
}) {
    val in // ...
}

In the first function, the incoming parameter is a completion. This, as I explained earlier, is actually a Subscribers.Completion enum specifying the completion message. There are two possible cases:

  • .finished — The publisher is telling us that there will be no more values forthcoming because it has finished producing values in good order. For example, a data task publisher sends a .finished completion after it has done whatever networking we asked it to do.

  • .failure — The publisher is telling us that there will be no more values forthcoming because it has encountered an unrecoverable failure. The nature of that failure is expressed as an Error object which is this enum case’s associated value.

In the second function, the incoming parameter is an actual value that came down the pipeline.

Here’s a minimal .sink implementation that acknowledges all possible outcomes:

.sink(receiveCompletion: {
    switch $0 {
        case .finished: print("finished in good order")
        case let .failure(err): print("failure!", err)
    }
}) { print($0) }

If the failure type of the publisher just upstream of the sink is Never, meaning that no failure can occur, it is permitted to omit the receiveCompletion parameter entirely. You don’t have to omit it; but if you have no use for a .finished completion, you might prefer to do so. In that case, we can rewrite our .sink implementation more simply:

.sink() { print($0) }

The salient feature of .sink is that you can do anything you want in its functions, especially the receiveValue function. A value has arrived at the end of the pipeline; what would you like to do with it? A .sink subscriber is the most flexible way to answer that question by responding to the arrival of a value.

NOTE: Sometimes, you’d like to do what sink does, but in an operator in the middle of the pipeline rather than in the subscriber at the end. A typical use case is that you want to respond secondarily to the arrival of a completion, either .finished or .failure, before the end of the pipeline. Combine has no .ensure operator similar to PromiseKit, but you can do something similar by misusing .handleEvents and implementing the receiveCompletion: parameter.


Table of Contents