Combine sink: ignore receiveValue, only completion is needed - swift

Consider the following code:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS")
}
} receiveValue: { value in
// this should be ignored
}
Just by looking at the CurrentValueSubject initializer, it's clear that the value is not needed / doesn't matter.
I'm using this particular publisher to make an asynchronous network request which can either pass or fail.
Since I'm not interested in the value returned from this publisher (there are none), how can I get rid of the receiveValue closure?
Ideally, the call site code should look like this:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS ")
}
}
It also might be the case that I should use something different other than AnyPublisher, so feel free to propose / rewrite the API if it fits the purpose better.
The closest solution I was able to find is ignoreOutput, but it still returns a value.

You could declare another sink with just completion:
extension CurrentValueSubject where Output == Void {
func sink(receiveCompletion: #escaping ((Subscribers.Completion<Failure>) -> Void)) -> AnyCancellable {
sink(receiveCompletion: receiveCompletion, receiveValue: {})
}
}

CurrentValueSubject seems a confusing choice, because that will send an initial value (of Void) when you first subscribe to it.
You could make things less ambiguous by using Future, which will send one-and-only-one value, when it's done.
To get around having to receive values you don't care about, you can flip the situation round and use an output type of Result<Void, Error> and a failure type of Never. When processing your network request, you can then fulfil the promise with .failure(error) or .success(()), and deal with it in sink:
let pub = Future<Result<Void, Error>, Never> {
promise in
// Do something asynchronous
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise(.success(.success(())))
//or
//promise(.success(.failure(error)))
}
}.eraseToAnyPublisher()
// somewhere else...
pub.sink {
switch $0 {
case .failure(let error):
print("Whoops \(error)")
case .success:
print("Yay")
}
}
You're swapping ugly code at one end of the chain for ugly code at the other, but if that's hidden away behind AnyPublisher and you're concerned with correct usage, that seems the way to go. Consumers can see exactly what to expect from looking at the output and error types, and don't have to deal with them in separate closures.

Related

Swift Combine sink stops receiving values after first error

Im moving my project to Combine from RxSwift
I have a logic where I want publisher to emit event every time I click button. Acrually clicking button executed pushMe.send()
pushMe
.print("Debug")
.flatMap { (res) -> AnyPublisher<Bool, Error> in
return Future<Bool, Error>.init { closure in
closure(.failure(Errors.validationFail))
}.eraseToAnyPublisher()
}
.sink(receiveCompletion: { completion in
print("Completion received")
}, receiveValue: { value in
print("Value = \(value)")
})
.store(in: &subscriptions)
The console result
Debug: receive value: (true)
Completion received
Debug: receive value: (true)
Debug: receive value: (true)
I do not understand why sink receive error only on first event. The rest clicks are ignored.
What does flatMap do -
Subscribes to the given publisher (let's say XPublisher).
Sends the Errors and Output values (not finished event/ completion) emitted
by XPublisher to the down stream.
So If you handle errors inside the flat map , (which means the publisher inside the flatMap does not emit errors), then flatMap Never sends an error to the down stream.
pushMe
.print("Debug")
.flatMap { (res) -> AnyPublisher<Bool, Never> in //<= here
return Future<Bool, Error>.init { closure in
closure(.failure(Errors.validationFail))
}
.replaceError(with: false) //<= here
.eraseToAnyPublisher()
}
.sink(receiveCompletion: { completion in
print("Completion received")
}, receiveValue: { value in
print("Value = \(value)")
})
.store(in: &subscriptions)
Otherwise you can handle error outside the fatMap. Problem here is that, once an error out whole the subscription / cancellable cancelled. ( in the below example error has replace with a false value)
pushMe
.print("Debug")
.flatMap { (res) -> AnyPublisher<Bool, Error> in
return Future<Bool, Error>.init { closure in
closure(.failure(Errors.validationFail))
}
.eraseToAnyPublisher()
}
.replaceError(with: false) //<= here
.sink(receiveCompletion: { completion in
print("Completion received")
}, receiveValue: { value in
print("Value = \(value)")
})
.store(in: &subscriptions)
What is happening in the above code.
FlatMap error outs.
replace the error with false (One false value will receive Because of this)
subscription cancelled because of the error out in the stream.
The rule is that if an error propagates down the pipeline, the entire pipeline is cancelled. Thus, if your Future generates an error, it passes as an error to the Sink and thus the pipeline is cancelled all the way up to the Publisher.
The pattern for preventing this is to deal with the error inside the FlatMap. Basically, you've got two pipelines here: the one that starts with pushMe and the one that starts with Future. Simply don't let the error generated by the Future pipeline "leak" out into the pushMe pipeline, and so the pushMe pipeline will not be cancelled. Instead, catch the error inside the FlatMap and, if you want to pass something out of it to your Sink, pass out of it some sort of value that tells your Sink that there has been a bad input.
A simple solution in your case would be to change the type your FlatMap to <Bool,Never>, and pass either true or false as the Bool to indicate whether validation succeeded in the Future or not.
Or, if it's important to you to pass more detailed information about the error down the pipeline, change the type of your FlatMap to <Result<Bool,Error>,Never> and package the error information into the .failure case of the Result object.
This is how Publishers work in Combine.
The Publisher can either emit values or emit a completion event - once a completion event was emitted, the Publisher is finished and it cannot emit any other values or another completion event. If the Publisher emits an error, the error is emitted as a failure completion, meaning that once an error is emitted, the Publisher completes and it cannot emit any more values.
There are several Combine operators designed for handling errors without completing the Publisher. Have a look into the catch operator for instance.
First, thanks all for helping with this question.
Answer of #matt is one of the possible solution.
Another solution is to create new pipeline every time you clicking button.
Im using this approach because I have sequence of steps below failing publisher and Im not able to rely of dummy true/false result further.
Just<String>()
.sink(receiveValue: { value in
startProcess()
.sink(receiveCompletion: { (completion:
Subscribers.Completion<Failure>) in
// can handle ALL types of error of pipe line in ONE place
}, receiveValue: { (v: P.Output) in
// handle correct result
})
})
.store(in: &subscriptions)
func startProcess() -> AnyPublisher<Bool, Error> {
Future<Bool, Error>.init { closure in
// action 1
closure(.success(true))
}
.flatMap { (b: Bool) -> AnyPubilsher<Void, Error> in
Future<Bool, Error>.init { closure in
// action 2
closure(.success(()))
}
}
}
Benefit is that you are able to handle all types of errors in one place if second sink()

Using Just with flatMap produce Failure mismatch. Combine

I have such code
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
and I'm getting compile error:
Instance method flatMap(maxPublishers:_:) requires the types
Just.Failure (aka Never) and Error be equivalent
And it's clear, because Just has Never as Failure and .flatMap requires Error as Failure, so Never != Error
I see 2 approaches:
using right Publisher, instead of Just, but I didn't find good candidate for this.
using some operator like .mapError, .mapError { $0 as Error }, but I'm not sure that it's great idea.
Any ideas how to handle it?
UPDATE:
it makes more sense to use
.setFailureType(to: Error.self)
or
.mapError { $0 as Error }
There is special operator setFailureType(to:). You can override failure type to whatever error type you need.
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.setFailureType(to: Error.self)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
https://developer.apple.com/documentation/combine/just/3343941-setfailuretype
If you call .mapError() on the Just output, it will change the type to include Error, but that closure will never be called (so I wouldn’t worry) — this is what I would do unless someone has a better idea.
Normally, the Self.Error == P.Error on flatMap makes sense, as you can’t just ignore errors coming from Self. But, when Self.Error is Never, you can ignore them and produce your own errors.
While the accepted answer certainly works it's pretty verbose. I stumbled on an alternative syntax using Result<Success,Failure).publisher that is somewhat more succinct:
Result.Publisher(.success(request))
(Note that in this case I'm depending on Swift to be able to infer the error type, but you might need to declare it explicitly: Result<URLRequest, Error>.Publisher(.success(request)))

Swift 5 Result type

In Swift 5 Apple introduced Result type. It's generic enum with two cases:
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
Personally I used to two separate completions in network calls success: Completion and failure: Completion, but from what I see now, Apple pushing us to use single completion with Result type and then inside perform switch. So what are advantages of this approach with Result? Because in a lot of cases I can just omit error handling and don't write this switch. Thanks.
You shouldn’t omit cases when Result is failure. You shouldn’t do it with Result and you shouldn’t do it with your closure for failure. You should handle errors.
Anyway, Result type was introduced for simplifing completion handlers. You can have single closure for handling success or failure (primary-opinion based if two separate closures are better or not). Also Result is designed for error handling. You can simply create your own enum conforming to Error and then you can create your own error cases.
Swift 5 introduced Result<Success, Failure> associated value enumeration[About] It means that your result can be either success or failure with additional info(success result or error object). Good practice is to manage error case and success case as atomic task.
Advantages:
Result is a single/general(generic)/full type which can be used for all yes/no cases
Not optional type
Forces consumers to check all cases
public enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
To use it
//create
func foo() -> Result<String, Error> {
//logic
if ok {
return .success("Hello World")
} else {
return .failure(.someError)
}
}
//read
func bar() {
let result = foo()
switch result {
case .success(let someString):
//success logic
case .failure(let error):
//fail logic
}
}

RxSwift ConnectableObservable missing autoconnect

Question
I'm porting a project that uses RxJava to RxSwift. There is a ConnectableObservable that uses autoconnect(). How would I port this to RxSwift? I'm looking for a similar feature or work around.
Information
I was looking to use refcount() in RxSwift but once the subscriber number goes to 0 it'll stop where autoconnect() would continue. According to these release notes, autoConnect works like only the first half of refCount, or more precisely, it is similar in behavior to refCount(), except that it doesn't disconnect when subscribers are lost.
I took #akamokd's advice, I just created my own. Since I only needed autoConnect() it's was pretty simple.
extension ConnectableObservableType {
func autoconnect() -> Observable<E> {
return Observable.create { observer in
return self.do(onSubscribe: {
_ = self.connect()
}).subscribe { (event: Event<Self.E>) in
switch event {
case .next(let value):
observer.on(.next(value))
case .error(let error):
observer.on(.error(error))
case .completed:
observer.on(.completed)
}
}
}
}
}

Completion handler never called

I have a function like this.
class func getAccountBalances(completionHandler:#escaping ((_ balances:Any) -> Void)){
//Alamofire request and we get the result. But sometimes the result fails.
switch response.result {
case .success(let value):
completionHandler(value)
case .failure(let error):
print ("error is: \(error)")
}
I am not putting code to handle the result if it fails. Is that a bad thing? Do I need to have a completion handler in the case that the call fails so that this function does not stay in the memory waiting for that completion handler to be called? What is the best practice?
In general, it is a good practice to call the completion on every case. The reason for this is that you usually want to let the upper lever(business logic layer) decide if it should mark some balances(for example) as saved, or maybe show a dialog when an error has occurred. That is a good practice with the thinking that everything should be a module. That being said, if another module will want to call the same function at some point, it may be a good thinking to let that module what happened with the result. That can be implemented in several ways, I won't enter here, it's your decision.
However, it's not a must to do it. If a block won't be called it should be deallocated, and then everything is good memory-wise. So in your example, if you don't retain the block somewhere else(for example holding it in a variable inside the class that makes getAccountBalances call), you should be just fine.
Another important part is when you call the function be careful to not create a memory leak where you retain the self inside the block:
getAccountBalances() { _ in
self.updateUI()
}
This block will create a retain to self and if everything goes okay with the call, but user left the screen, you may end up using variables that were deallocated and crash the app. A good practice here is to not retain the self in the callback, but make it weak before that:
getAccountBalances() { [weak self] _ in
// This is not necessarily needed, and you could use self?.updateUI() instead.
// However, this is usually another thing i like to do and consider it a good practice
guard let `self` = self else { return }
self.updateUI()
}
Your title: Completion handler never called
Your code: does not return any value to the completion handler if there is an error.
How would you expect to see a result of the competion handler if there is a failure? Would you like to crash the app? This way is better because it handles both cases:
class func getAccountBalances(completionHandler:#escaping ((_ balances:Any?) -> Void)){ //make Any an optional
//Alamofire request and we get the result. But sometimes the result fails.
switch response.result {
case .success(let value):
completionHandler(value)
case .failure(let error):
print ("error is: \(error)")
completionHandler(nil)
}
Your new call to this function:
getAccountBalances() { value in
guard let _value = value else { // anticipate on the error return }
// use _value, the balances are inside.
}
An other approach would be not making it nil, but downcasting the value inside of it. That would look like this:
class func getAccountBalances(completionHandler:#escaping ((_ balances:Any) -> Void)){
//Alamofire request and we get the result. But sometimes the result fails.
switch response.result {
case .success(let value):
completionHandler(value)
case .failure(let error):
print ("error is: \(error)")
completionHandler(error) //notice this change
}
Then your function would look like this:
getAccountBalances() { value in
if let error = value as? Error { //or whatever type your error is in the function
//there is an error
}
}