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!


One-Shot Subscribers

As I explained in the previous section, we need to store our subscriber in persistent storage so that the pipeline lives long enough for the publisher to have a chance to publish at some time in the future, whenever that may be. After that, however, a publisher that only publishes once — a one-shot publisher — has done its work, and can be permitted to go out of existence.

In a situation like that, we only need our subscriber to persist long enough to receive a value or a completion (or both). It is a one-shot subscriber. I’m going to show you a little trick that I use to make a one-shot subscriber, without having to go to all the trouble of preparing an instance property and calling store(in:).

Let’s start with our usual one-shot publisher, a data task publisher. This is the same code you’re already so familiar with:

let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
let pub : AnyPublisher<UIImage?,Never> =
    URLSession.shared.dataTaskPublisher(for: url)
        .map {$0.data}
        .replaceError(with: Data())
        .compactMap { UIImage(data:$0) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()

Now comes the interesting part. Watch closely:

var cancellable: AnyCancellable? // 1
cancellable = pub.sink(receiveCompletion: {_ in // 2
    cancellable?.cancel() // 3
}) { image in
    self.imageView.image = image
}

Do you see what I did there? Perhaps not, so I’ll explain it:

  1. First, I declare a local AnyCancellable variable; for reasons having to do with the rules of Swift syntax, this needs to be an Optional.

  2. Then, I create my subscriber and set my AnyCancellable variable to that subscriber. Again, for reasons having to do with the rules of Swift syntax, my subscriber needs to be a Sink.

  3. Finally, in the subscriber itself, I cancel the AnyCancellable when I receive the completion.

The cancellation in the third step actually does two things quite apart from calling cancel() — things having to do with memory management:

  • By referring to cancellable inside the asynchronous completion function of the Sink, I keep cancellable and the whole pipeline alive long enough for a value to arrive from the subscriber.

  • By cancelling cancellable, I permit the pipeline to go out of existence and prevent a retain cycle that would cause the surrounding view controller to leak.


Table of Contents