Swift Combine How Set<Anycancellable> Works

Swift Combine how SetAnyCancellable works?

No, you dont need to cancel any cancellable because this kind of object calls the method cancel when deinitialized. So your code is correct.

Apple's documentation of AnyCancellable:

An AnyCancellable instance automatically calls cancel() when deinitialized

https://developer.apple.com/documentation/combine/anycancellable

Combine: Cancel a SetAnyCancellable or remove all its stored objects to stop the subscriptions

If there are strong references to any of the cancellables in the set, that implies that other parties are interested in them, and therefore they should not be cancelled explicitly.

By removing the reference, you're saying that the owner of observations is no longer interested in those cancellables. If that was the only owning reference, then cancel will happen automatically.

If you explicitly cancel each cancellable and you have other strong references by mistake, then you might be masking the effect of those mistakes, making them harder to find. If you have other strong references on purpose, you're going to have things being cancelled that you don't expect. So just empty the set.

AnyCancellable.store(in:) with Combine

Dávid Pásztor's solution is close, but it has a race condition. Specifically, suppose the newPhotos publisher completes synchronously, before the sink method returns. Some publishers operate this way. Just and Result.Publisher both do, among others.

In that case, the completion block runs before sink returns. Then, sink returns an AnyCancellable which is stored in newPhotosSubscription. But the subscription has already completed, so newPhotosSubscription will never be set back to nil.

So for example if you use a URLSession.DataTaskPublisher in live code but substitute a Just publisher in some test cases, the tests could trigger the race condition.

Here's one way to fix this: keep track of whether the subscription has completed. Check it after sink returns, before setting newPhotosSubscription.

private var ticket: AnyCancellable? = nil

if ticket == nil {
var didComplete = false
let newTicket = newPhotos
.sink(
receiveValue: { [weak self] in
self?.images.value.append($0)
},
receiveCompletion: { [weak self] _ in
didComplete = true
self?.ticket = nil
}
)
if !didComplete {
ticket = newTicket
}
}

Everytime I call the method again, it override the AnyCancellable, what happens to the previous one? Does it still complete before being deallocated?

The previous one, if any, is cancelled, because the only reference to the old AnyCancellable is destroyed and so the AnyCancellable is destroyed. When an AnyCancellable is destroyed, it cancels itself (if not already cancelled).

Advantage of AnyCancellable.store(in:) over SetAnyCancellable.insert(:)

If you know that you'll only need one reference to a AnyCancellable, then you can store a direct reference to that and avoid the Set<AnyCancellable>

Otherwise, there is no difference aside from the stylistic one. I prefer store(in:) to avoid the need to use a local variable to maintain readability. (Of course, you could wrap the return value of your pipeline in a Controller.cancellables.insert but that's just downright ugly IMO.)

I would still recommend using the store(in:) approach so you don't need to change your implementation if you do end up needing to hold onto more AnyCancellable references.

How to stop storing AnyCancellable after Swift Combine Sink has received at least one value?

If you just need to keep it alive until the sink is called once, you can just create a temporary variable

var cancellable: AnyCancellable?
cancellable = $locationState.sink { locationState in
if let locationState = locationState {
invokeCallbackWithLocationState(locationState)
}
cancellable = nil
}

This will retain the AnyCancellable long enough (because the closure retains the reference)

With Combine, how to deallocate the Subscription after a network request

Below is some code that does this. It's kind of awkward, and it may not even be correct. I can imagine a race condition where network request could start and complete on another thread before sub is set to the non-nil value.

Danger! Swift.Set is not thread safe. If you want to access a Set from two different threads, it is up to you to serialize the accesses so they don't overlap.

What is possible in general (although not perhaps with URLSession.DataTaskPublisher) is that a publisher emits its signals synchronously, before the sink operator even returns. This is how Just, Result.Publisher, Publishers.Sequence, and others behave. So those produce the problem you're describing, without involving thread safety.

Now, how to solve the problem? If you don't think you want to actually be able to cancel the subscription, then you can avoid creating an AnyCancellable at all by using Subscribers.Sink instead of the sink operator:

        URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: MyResponse.self, decoder: JSONDecoder())
.subscribe(Subscribers.Sink(
receiveCompletion: { completion in ... },
receiveValue: { response in ... }
))

Combine will clean up the subscription and the subscriber after the subscription completes (with either .finished or .failure).

But what if you do want to be able to cancel the subscription? Maybe sometimes your SomeThing gets destroyed before the subscription is complete, and you don't need the subscription to complete in that case. Then you do want to create an AnyCancellable and store it in an instance property, so that it gets cancelled when SomeThing is destroyed.

In that case, set a flag indicating that the sink won the race, and check the flag before storing the AnyCancellable.

        var sub: AnyCancellable? = nil
var isComplete = false
sub = URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: MyResponse.self, decoder: JSONDecoder())
// This ensures thread safety, if the subscription is also created
// on DispatchQueue.main.
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
isComplete = true
if let theSub = sub {
self?.subs.remove(theSub)
}
},
receiveValue: { response in ... }
}
if !isComplete {
subs.insert(sub!)
}


Related Topics



Leave a reply



Submit