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 deinitializedhttps://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
How to Find The Nth Root of a Value
How to Test a Url and Get a Status Code in Swift 3
Get Png Representation of Nsimage in Swift
Switch to Match Multiple Cases from Optionsettype
Extend All Number Types in Swift
Decoding a Nested Array in Swift 4
Save/Get UIcolor from Userdefaults
Calling Git Commands from Within a Swift Macos Application
Key Data Not Storing to Icloud with Nsubiquitouskeyvaluestore.Defaultstore
The #Selector Is Not Compatible with The Closure
How to Group Items in a Nspopupbutton
All of My UIalertcontroller Messages Became Single Line
How to Create a Smooth Colour Change Animation Using Swiftui? (Example in Question)
Swift Codable: Decode Dictionary with Unknown Keys