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.

Richard Topchii
  • 7,075
  • 8
  • 48
  • 115
  • What's wrong with using `} receiveValue: { _ in }` at the end. Interface contract of `sink` with no optionals so parameters must be specified. – Asperi Sep 24 '21 at 08:00
  • 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`. – Sweeper Sep 24 '21 at 08:00
  • 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 }`. – Richard Topchii Sep 24 '21 at 08:05

2 Answers2

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: {})
    }
}
LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
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.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • It's actually a Future out of the other part of the API. So the `CurrentValueSubject` 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. – Richard Topchii Sep 27 '21 at 17:46
  • 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 tradeoffs – Richard Topchii Sep 27 '21 at 18:22
  • You can map the Result to Result but yes, it's all about tradeoffs – jrturton Sep 28 '21 at 08:23
  • 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(())))` – Richard Topchii Sep 28 '21 at 08:30