What exactly is a Combine AnyCancellable?

Published on: August 24, 2021

If you've worked with Combine in your applications you'll know what it means when I tell you that you should always retain your cancellables. Cancellables are an important part of working with Combine, similar to how disposables are an important part of working with RxSwift. Interestingly, Swift Concurrency's AsyncSequence operates without an equivalent to cancellable (with memory leaks as a result). That said, in this post we'll only focus on Combine.

For example, you might have built a publisher that wraps CLLocationManagerDelegate and exposes the user's current location with a currentLocation publisher that's a CurrentValueSubject<CLLocation, Never>. Subscribing to this publisher would look look a bit like this:

struct ViewModel {
    let locationProvider: LocationProvider
    var cancellables = Set<AnyCancellable>()

  init(locationProvider: LocationProvider) {
        self.locationProvider = locationProvider
        locationProvider.currentLocation.sink { newLocation in 
            // use newLocation
        }.store(in: &cancellables)
    }
}

For something that's so key to working with Combine, it kind of seems like cancellables are just something we deal with without really questioning it. Thats why in this post, I'd like to take a closer look at what a cancellable is, and more specifically, I'd like to look at what the enigmatic AnyCancellable that's returned by both sink and assign(to:on:) is exactly.

Understanding the purpose of cancellables in Combine

Cancellables in Combine fulfill an important part in Combine's subscription lifecycle. According to Apple, the Cancellable protocol is the following:

A protocol indicating that an activity or action supports cancellation.

Ok. That's not very useful. I mean, if supporting cancellation is all we want to do, why do we need to retain our cancellables?

If we look at the detailed description for Cancellable, you'll find that it says the following:

Calling cancel() frees up any allocated resources. It also stops side effects such as timers, network access, or disk I/O.

This still isn't great, but at least it's something. We know that an object that implements Cancellable has a cancel method that we can call to stop any in progress work. And more importantly, we know that we can expect any allocated resources to be freed up. That's really good to know.

What this doesn't really tell us is why we need to retain our cancellables in Combine. Based on the information that Apple provides there's nothing that even hints towards the need to retain cancellables.

Let's take a look at the documentation for AnyCancellable next. Maybe a Cancellable and AnyCancellable aren't quite the same even though we'd expect AnyCancellable to be nothing more than a type-erased Cancellable based on the way Apple chose to name it.

The short description explains the following:

A type-erasing cancellable object that executes a provided closure when canceled.

Ok. That's interesting. So rather it being "just" a type erased object that conforms to Cancellable, we can provide a closure to actually do something when we initialize an AnyCancellable. When we subscribe to a publisher we don't create our own AnyCancellable though, so we'll need to dig a little deeper.

There's once sentence in the AnyCancellable documentation that tells us exactly why we need to retain cancellables. It's the very last sentence in the discussion and it reads as follows:

An AnyCancellable instance automatically calls cancel() when deinitialized.

So what exactly does this tell us?

Whenever an AnyCancellable is deallocated, it will call cancel() on itself. This will run the provided closure that I mentioned earlier. It's safe to assume that this closure will ensure that any resources associated with our subscription are torn down. After all, that's what the cancel() method is supposed to do according to the Cancellable protocol.

Based on this, we can deduce that the purpose of cancellables in Combine, or rather the purpose of AnyCancellable in Combine is to associate the lifecycle of a Combine subscription to something other than the subscription completing.

When we retain a cancellable in an instance of a view model, view controller, or any other object, the lifecycle of that subscription becomes connected to that of the owner (the retaining object) itself. Whenever the owner of the cancellable is deallocated, the subscription is torn down and all resources are freed up immediately.

Note that this might not be quite intuitive when you think of that original description I quoted from the Cancellable documentation:

A protocol indicating that an activity or action supports cancellation.

Cancelling a subscription by calling cancel() on an AnyCancellable is not a graceful operation. This is already hinted at because the documentation for Cancellable mentions that "any allocated resources" will be freed up. You need to interpret this broadly.

You won't just cancel an in flight network call and be notified about it in a receiveCompletion closure. Instead, the entire subscription is torn down immediately. You will not be informed of this, and you will not be able to react to this in your receiveCompletion closure.

To sum up the purpose of cancellables in Combine, they are used to tie the lifecycle of a subscription to the object that retains the cancellable that we receive when we subscribe to a publisher.

This description might lead to you thinking that an AnyCancellable is a wrapper for a subscription. Unfortunately, that's not quite accurate. It's also not flat out wrong, but there's a bit of a nuance here; Apple chose the name AnyCancellable instead of Subscription on purpose.

What's inside an AnyCancellable exactly?

If an AnyCancellable isn't a subscription, then what it is? What's inside of an AnyCancellable?

The answer is complicated...

When I first learned Combine I was lucky enough to run into an Apple employee at a conference. We got talking about Combine, and I explained that I was working on a Combine book. I started firing off a few questions to validate my understanding of Combine and I was very lucky to get an answer or two.

One of my questions was "So is an AnyCancellable a subscription then?" and the answer was short and simple "No. It's an AnyCancellable".

You might think that's unhelpful, and I would fully understand. However, the answer is fully correct as I learned in our conversation and it makes Apple's intent with AnyCancellable perfectly clear.

Combine intentionally does not specify what's inside of AnyCancellable because we simply don't need to know exactly what is wrapped and how. All we need to know is that an AnyCancellable conforms to the Cancellable protocol, and when its cancel() method is called, all resources retained by whatever the Cancellable wrapper are released.

In practice, we know that an AnyCancellable will most likely wrap an object that conforms to Subscription and possibly also one that conforms to Subscriber. One of the two might even have a reference to a Publisher object.

We know this because we know that these three objects are always involved when you subscribe to a publisher. I've outlined this in more detail in this post as well as my Combine book.

This is really a long-winded way of me trying to tell you that we don't know what's inside an AnyCancellable, and it doesn't matter. You just need to remember that when an AnyCancellable is deallocated it will run its cancellation closure which will tear down anything it retains. This includes tearing down your subscription to a publisher.

If you're interested in learning about Swift Concurrency's AsyncSequence, and how it compares to publishers in Combine, I highly recommend that you start by looking at this post.

In Summary

In this post you learned about a key aspect of Combine; the Cancellable. I explained what the Cancellable protocol is, and from there I moved on to explain what the AnyCancellable is.

You learned that subscribing to a publisher with sink or assign(to:on:) will return an AnyCancellable that will tear down your subscription whenever the AnyCancellable is deallocated. This makes sure that your subscription to a publisher is deallocated when the object that retains your AnyCancellable is deallocated. This prevents your subscriptions from being deallocated immediately when the scope where they're created exits.

Lastly, I explained that we don't know what exactly is inside of the AnyCancellable objects that we retain for our subscriptions. While we can be pretty certain that an AnyCancellable must somehow retain a subscription, we shouldn't refer to it as a wrapper for a subscription because that would be inaccurate.

Hopefully this post gave you some extra insights into something that everybody that works with Combine has to deal with even though there's not a ton of information out there on AnyCancellable specifically.

Categories

Combine

Subscribe to my newsletter