0

I have a source publisher, and have an another publisher rely source publisher. This is playground test code for my situation:

import Foundation
import Combine
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

typealias Image = Int

enum NetError: Error {
    case invalidImage
}

func convertImageToVideo(_ image: Image) -> AnyPublisher<Image, NetError> {
    Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            if image == 20 {
                promise(.failure(.invalidImage))
            } else {
                promise(.success(image))
            }
        }
    }
    .eraseToAnyPublisher()
}

var image = PassthroughSubject<Image, NetError>()

let subscription = image
    .map { image in
        convertImageToVideo(image)
    }
    .switchToLatest()
    .sink { completion in
        if case let .failure(error) = completion {
            print("Receive error: \(error)")
        }
    } receiveValue: { video in
        print("Receive new video: \(video)")
    }

image.send(0)
image.send(20)
image.send(40)

DispatchQueue.main.async {
    image.send(20)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    print("Send 50 into image.")
    image.send(50)
}

But I only receive one error in console:

Receive error: invalidImage

This is not ideal, I want continue receive value even if convertImageToVideo method occur an error. So I change code:

import Foundation
import Combine
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

typealias Image = Int

enum NetError: Error {
    case invalidImage
}

func convertImageToVideo(_ image: Image) -> AnyPublisher<Image, NetError> {
    Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            if image == 20 {
                promise(.failure(.invalidImage))
            } else {
                promise(.success(image))
            }
        }
    }
    .eraseToAnyPublisher()
}

var image = PassthroughSubject<Image, NetError>()

let subscription = image
    .map { image -> AnyPublisher<Result<Image, NetError>, Never> in
        convertImageToVideo(image)
            .map { video in
                .success(video)
            }
            .catch({ error in
                Just(.failure(error))
            })
            .eraseToAnyPublisher()
    }
    .switchToLatest()
    .sink { completion in
        if case let .failure(error) = completion {
            print("Receive error: \(error)")
        }
    } receiveValue: { video in
        print("Receive new video: \(video)")
    }

image.send(0)
image.send(20)
image.send(40)

DispatchQueue.main.async {
    image.send(20)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    print("Send 50 into image.")
    image.send(50)
}

This time output is ideal:

Receive new video: failure(__lldb_expr_64.NetError.invalidImage)
Send 50 into image.
Receive new video: success(50)

But the error not come from completion closure, instead of it come from new value closure, I must handle error from complete closure and new value closure. Anyone has good idea? Thanks!

sunny
  • 43
  • 5
  • 1
    What do you mean by "I must handle error from `complete` closure"? Why do you _have_ to do that? Note that a publisher can fail _at most once_. Once a publisher fails, it "completes" and can't publish anything else. The two things that you want (handle errors in `completion`, and have it still publish values after failing) are mutually exclusive. – Sweeper Aug 24 '21 at 08:09
  • "But the error not come from `completion` closure" <-- Of course, because the publisher has not completed. It still has values to publish, which is what you want, right? If it had come from `completion`, the publisher would have completed, and there would be no more values for you to receive. – Sweeper Aug 24 '21 at 08:11
  • @Sweeper Thanks for your reply, I'm wrong, I'm not understand what error mean before. And for my situation do you have any suggest for code? – sunny Aug 24 '21 at 08:30
  • I would just use something similar to your second version of the code, mapping the error to a `Result`. – Sweeper Aug 24 '21 at 08:33

1 Answers1

1

The problem you are running into is that when your image publisher runs into an error, it finishes the sequence. What you really want flowing through your sequence is a Result saying whether or not each image conversion worked. To do that, convertImageToVideo needs to emit Result values, not just image values. This leads to some pretty funky looking results from your Future because it the future always succeeds and then tells you whether or not the conversion worked.

Here's your code, reworked so the results are flowing through:

import Foundation import Combine import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

typealias Image = Int

enum NetError: Error {
    case invalidImage
}

func convertImageToVideo(_ image: Image) -> AnyPublisher<Result<Image,Error>, Never> {
    Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            if image == 20 {
                promise(.success(.failure(NetError.invalidImage)))
            } else {
                promise(.success(.success(image)))
            }
        }
    }
    .eraseToAnyPublisher()
}

var image = PassthroughSubject<Image, Never>()

let subscription = image
    .flatMap { image in
        convertImageToVideo(image)
    }
    .sink { video in
        switch video {
            case .success(let video):
                print("Receive new video: \(video)")
            case .failure(let error):
                print("Receive error: \(error)")
        }
    }

image.send(0)
image.send(20)
image.send(40)

DispatchQueue.main.async {
    image.send(20)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    print("Send 50 into image.")
    image.send(50)
}

And the output is:

Receive new video: 0
Receive error: invalidImage
Receive error: invalidImage
Receive new video: 40
Send 50 into image.
Receive new video: 50
Scott Thompson
  • 22,629
  • 4
  • 32
  • 34