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:
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 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 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 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.
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.
Top comments (0)