Swift Combine Publishers VS Completion Handler and When to Cancel

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



Leave a reply



Submit