Home > OS >  Combine sink: ignore receiveValue, only completion is needed
Combine sink: ignore receiveValue, only completion is needed

Time:09-24

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.

CodePudding user response:

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: {})
    }
}

CodePudding user response:

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