Combine sink: ignore receiveValue, only completion is needed
Asked Answered
B

2

8

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.

Blamable answered 24/9, 2021 at 7:46 Comment(3)
What's wrong with using } receiveValue: { _ in } at the end. Interface contract of sink with no optionals so parameters must be specified.Sooksoon
Are you just picky about the aesthetics? I don't think there's anything like that built-in, but you can always write your sink overload that takes no receiveValue parameter in a Publisher extension. Also, consider using a Future, rather than CurrentValueSubject.Garlic
Yeah, I'm just not sure if I'm even using the right API for the task. Is AnyPublisher a good fit for this use-case? If there aren't any better option, I'll stick to your suggestion of using } receiveValue: { _ in }.Blamable
C
0

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.

Cronyism answered 24/9, 2021 at 8:42 Comment(4)
It's actually a Future out of the other part of the API. So the CurrentValueSubject<Void, Error> I've invented just for the sake of example. It could be AnyPublisher. But under the hood it's actually a future, so your example seems to work well. The correctness at the call site is preferred.Blamable
The drawback of this approach is that I cannot type-erase the Error later in the chain by using .mapError({$0 as Error}), so both have their tradeoffsBlamable
You can map the Result<Void, Error> to Result<Void, OtherError> but yes, it's all about tradeoffsCronyism
I've resolved that issue by erasing the type of the error in the API. Still, in the end I didn't go with this API due to it's complexity: ` promise(.success(.success(())))`Blamable
U
6

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: {})
    }
}
Usage answered 24/9, 2021 at 9:9 Comment(0)
C
0

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.

Cronyism answered 24/9, 2021 at 8:42 Comment(4)
It's actually a Future out of the other part of the API. So the CurrentValueSubject<Void, Error> I've invented just for the sake of example. It could be AnyPublisher. But under the hood it's actually a future, so your example seems to work well. The correctness at the call site is preferred.Blamable
The drawback of this approach is that I cannot type-erase the Error later in the chain by using .mapError({$0 as Error}), so both have their tradeoffsBlamable
You can map the Result<Void, Error> to Result<Void, OtherError> but yes, it's all about tradeoffsCronyism
I've resolved that issue by erasing the type of the error in the API. Still, in the end I didn't go with this API due to it's complexity: ` promise(.success(.success(())))`Blamable

© 2022 - 2024 — McMap. All rights reserved.