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!
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 thereceiveCompletion:
parameter.