What exactly is a Combine AnyCancellable?
Tags: swift, combine, frp
Published: 2021-08-24 18:41:11

If you've worked with Combine at all 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.

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>`. If you subscribe to this publisher it might look a bit like this:

```swift
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](https://developer.apple.com/documentation/combine/cancellable), the `Cancellable` protocol is the following:

> A protocol indicating that an activity or action supports cancellation.

Ok. That's not very useful per se. 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](https://www.donnywals.com/understanding-type-erasure-in-swift/) `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 save 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.

So. 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 was first learning 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 kind of started firing off a few questions to validate my understanding of Combine and I was very lucky to kind of 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.

Combine intentionally does not specify what's inside of a cancellable 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](https://www.donnywals.com/understanding-combines-publishers-and-subscribers/) as well as my [Combine book](https://practicalcombine.com).

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.

## 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.