Swift Combine publishers vs completion handler and when to cancel
Instead of using the .sink
operator, you can use the Sink
subscriber directly. That way you don't receive an AnyCancellable
that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}
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.
Combine Publisher does not cancel when subscribed to on a background queue
The quote from the documentation is not a statement about cancellation, it's a statement about threading. It says: if you subscribe on a certain queue, then that is the queue that will be used when/if the time comes for the cancellation message to be sent up the pipeline.
By choosing to subscribe on a specified queue, you are explicitly saying: when the time comes to cancel, queue that call. So, as with any action on a queue, we now have no idea when that will actually happen. The claim that "this call should not have happened" is thus wrong; there is no "should have" in the story. On the contrary, any expectation as to when cancellation would percolate up to the publisher is exactly what you gave up when you subscribed on the queue.
(Observe, by the way, that the Completion arrives down the pipeline in good order at the expected moment — that is, the sink gets the Alpha value followed immediately by the .finished
Completion. It is only the publisher that you have given all this extra leeway to.)
How to have Combine subscriber finish upon receiving specific value
The solution I came up with uses the tryPrefix
operator:
let FavoriteNumber = 42
let publisher: AnyPublisher<Int, Never> = (0...360).publisher.eraseToAnyPublisher()
let upToFavoriteNum = publisher.tryPrefix {
$0 != FavoriteNumber
}
let subscription = upToFavoriteNum.sink(receiveCompletion: {
completion in
debugPrint(completion)
}, receiveValue: {
print("processing \($0)")
})
Find Canceled Publisher Combine
There is no way to check if an AnyCancellable
is active or not. Cancellable
(whose type erased form AnyCancellable
is) only requires a cancel()
method, there's no state handling requirement by the protocol.
However, Cancellable
calls cancel
when it is deallocated, so the best way to handle subscriptions in Combine is to throw away all references to AnyCancellable
instances whenever you want to cancel a subscriptions.
So for your specific use case, the best approach is to only hold a single AnyCancellable
instance, since it probably doesn't make much sense anyways to have several subscriptions to the same publisher.
You can decide if on subsequent calls to setupSocket
you want to create a new subscription or simply do nothing and keep the previous one alive (however, I'd suggest doing the latter, since you're working with sockets).
private var socketSubscription: AnyCancellable?
func setupSocket() {
// We only want 1 subscription
guard socketSubscription == nil else { return }
SocketHelper.shared.startListene()
socketSubscription = SocketHelper.shared.publisher.sink {[unowned self] (sub) in
print(sub)
print("FINISH ")
} receiveValue: {[unowned self] (value) in
print("SUBSCRBE GOT VALUE ")
}
}
Swift Combine Completion Handler with return of values
You're trying to combine Combine with old asynchronous code. You can do it with Future
, check out more about it in this apple article:
Future { promise in
signinModel.login { success in
if success == true {
promise(Result.success(()))
}
else {
promise(Result.failure(Error.unknown))
}
}
}
.flatMap { _ in
// repeat request if login succeed
request(ofType: type, from: endpoint, body: body)
}.eraseToAnyPublisher()
But this should be done when you cannot modify the asynchronous method or most of your codebase uses it.
In your case it looks like you can rewrite login
to Combine. I can't build your code, so there might be errors in mine too, but you should get the idea:
func login() -> AnyPublisher<Void, Error> {
self.loginState = .loading
let preparedBody = APIPrepper.prepBody(parametersDict: ["username": self.credentials.username, "password": self.credentials.password])
return service.request(ofType: UserLogin.self, from: .login, body: preparedBody)
.handleEvents(receiveCompletion: { res in
if case let .failure(error) = res {
(self.banner.message,
self.banner.stateIdentifier,
self.banner.type,
self.banner.show) = (error.errorMessage, error.statusCode, "error", true)
self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
}
})
.flatMap { loginResult in
if loginResult.token != nil {
self.loginState = .success
self.token.token = loginResult.token!
_ = KeychainStorage.saveCredentials(self.credentials)
_ = KeychainStorage.saveAPIToken(self.token)
return Just(Void()).eraseToAnyPublisher()
} else {
(self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = ("ERROR",
"TOKEN",
"error",
true)
self.loginState = .failed(stateIdentifier: "TOKEN", errorMessage: "ERROR")
return Fail(error: Error.unknown).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
And then call it like this:
signinModel.login()
.flatMap { _ in
request(ofType: type, from: endpoint, body: body)
}.eraseToAnyPublisher()
How can I subscribe again after finishing with Combine?
Once you send a completion, the Publisher is done. So, if you want to subscribe again and get new events, you'll need a new instance of that Publisher.
var subject = PassthroughSubject<String, Never>() //<-- var instead of let
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
subject.send("test1")
subject.send(completion: .finished)
subject = PassthroughSubject<String, Never>() //<-- Here
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
subject.send("test2")
Related Topics
Nswindow with Round Corners in Swift
Prevent Error "Funk" Sound in Event Monitor Os X
How to Get Data from a Swift Nsurlsession
Consuming a Soap Web Service with Swift
Bit Field Larger Than 64 Shifts in Swift
Protocol Extension Initializer Forcing to Call Self.Init
Swift Class Doesn't Like Self.View.Addsubview()
How to Immediately See Swift Errors in Appcode
How to Retrieve All Contacts Using Cncontact.Predicateforcontacts
Swift Protocol with Associated Type - Type May Not Reference Itself as a Requirement
Scntext Alignment Not Working in iOS
Why iOS13 Login with Facebook Does Not Work