Combine: how to replace/catch an error without completing the original publisher?

给你一囗甜甜゛ 提交于 2020-11-28 06:44:40

问题


Given the following code:

    enum MyError: Error {
        case someError
    }

    myButton.publisher(for: .touchUpInside).tryMap({ _ in
        if Bool.random() {
            throw MyError.someError
        } else {
            return "we're in the else case"
        }
    })
        .replaceError(with: "replaced Error")
        .sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
        }).store(in: &cancellables)

Whenever I tap the button, I get we're in the else case until Bool.random() is true - now an error is thrown. I tried different things, but I couldn't achieve to catch/replace/ignore the error and just continue after tapping the button.

In the code example I would love to have e.g. the following output

we're in the else case
we're in the else case
replaced Error
we're in the else case
...

instead I get finished after the replaced error and no events are emitted.

Edit Given a publisher with AnyPublisher<String, Error>, how can I transform it to a AnyPublisher<String, Never> without completing when an error occurs, i.e. ignore errors emitted by the original publisher?


回答1:


I believe E. Coms answer is correct, but I'll state it much simpler. The key to handling errors without causing the pipeline to stop processing values after an error is to nest your error-handling publisher inside of flatMap:

import UIKit
import Combine

enum MyError: Error {
  case someError
}

let cancel = [1,2,3]
  .publisher
  .flatMap { value in
    Just(value)
      .tryMap { value throws -> Int in
        if value == 2 { throw MyError.someError }
        return value
    }
    .replaceError(with: 666)
  }
  .sink(receiveCompletion: { (completed) in
    print(completed)
  }, receiveValue: { (sadf) in
    print(sadf)
  })

Output:

1
666
3
finished

You can run this example in a playground.


Regarding the OP's edit:

Edit Given a publisher with AnyPublisher<String, Error>, how can I transform it to a AnyPublisher<String, Never> without completing when an error occurs, i.e. ignore errors emitted by the original publisher?

You can't.




回答2:


There was a WWDC movie mentioned, and I believe it's "Combine in Practice" from 2019, start watching around 6:24: https://developer.apple.com/wwdc19/721

Yes, .catch() terminates the upstream publisher (movie 7:45) and replaces it with a given one in the arguments to .catch thus usually resulting in .finished being delivered when using Just() as the replacement publisher.

If the original publisher should continue to work after a failure, a construct involving .flatMap() is requried (movie 9:34). The operator resulting in a possible failure needs to be executed within the .flatMap, and can be processed there if necessary. The trick is to use

.flatMap { data in
    return Just(data).decode(...).catch { Just(replacement) }
}

instead of

.catch { return Just(replacement) } // DOES STOP UPSTREAM PUBLISHER

Inside .flatMap you always replace the publisher and thus do not care if that replacement publisher is terminated by .catch, since its already a replacement and our original upstream publisher is safe. This example is from the movie.

This is also the answer to your Edit: question, on how to turn a <Output, Error> into <Output, Never>, since the .flatMap does not output any errors, its Never before and after the flatMap. All error-related steps are encapsulated in the flatMap. (Hint to check for Failure=Never: if you get Xcode autocompletion for .assign(to:) then I believe you have a Failure=Never stream, that subscriber is not available otherwise. And finally the full playground code

PlaygroundSupport.PlaygroundPage.current.needsIndefiniteExecution = true

enum MyError: Error {
    case someError
}
let cancellable = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect()
    .flatMap({ (input) in
        Just(input)
            .tryMap({ (input) -> String in
                if Bool.random() {
                    throw MyError.someError
                } else {
                    return "we're in the else case"
                }
            })
            .catch { (error) in
                Just("replaced error")
        }
    })
    .sink(receiveCompletion: { (completion) in
        print(completion)
        PlaygroundSupport.PlaygroundPage.current.finishExecution()
    }) { (output) in
        print(output)
}



回答3:


I suggest using Publisher with typealias Failure = Never and output as an optional Result: typealias Output = Result<YourSuccessType, YourFailtureType>




回答4:


To do this you can use the catch operator and Empty publisher:

let stringErrorPublisher = Just("Hello")
    .setFailureType(to: Error.self)
    .eraseToAnyPublisher() // AnyPublisher<String, Error>

let stringPublisher = stringErrorPublisher
    .catch { _ in Empty<String, Never>() }
    .eraseToAnyPublisher() // AnyPublisher<String, Never>



回答5:


The Publisher emits until it completes or fails (with an error), after that the stream will be terminated.

One way to overcome this is to use a Result as a Publisher type

Publisher

protocol SerivceProtocol {
    var value: Published<Result<Double, MyError>>.Publisher { get }
}

Subscriber

service.value
        .sink { [weak self] in
            switch $0 {
            case let .success(value): self?.receiveServiceValue(value)
            case let .failure(error): self?.receiveServiceError(error)
            }
        }
        .store(in: &subscriptions)



回答6:


Just insert flatMap as following and you can achieve what your want

   self.myButton.publisher(for: \.touchUpInside).flatMap{
            (data: Bool) in
        return Just(data).tryMap({ _ -> String in
        if Bool.random() {
            throw MyError.someError
        } else {
            return "we're in the else case"
        }}).replaceError(with: "replaced Error")
    }.sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
       }).store(in: &cancellables)

The working model seems like this:

 Just(parameter).
 flatMap{ (value)->AnyPublisher<String, Never> in 
 return MyPublisher(value).catch { <String, Never>() } 
 }.sink(....)

If we use the above example, it could be like this:

let firstPublisher    = {(value: Int) -> AnyPublisher<String, Error> in
           Just(value).tryMap({ _ -> String in
           if Bool.random() {
               throw MyError.someError
           } else {
               return "we're in the else case"
            }}).eraseToAnyPublisher()
    }

    Just(1).flatMap{ (value: Int) in
        return  firstPublisher(value).replaceError(with: "replaced Error")
   }.sink(receiveCompletion: { (completed) in
            print(completed)
        }, receiveValue: { (sadf) in
            print(sadf)
       }).store(in: &cancellables)

Here, you can replace the firstPublisher with AnyPublisher that takes one parameter.

Also Here, the firstPublisher only has one value, it only can produce one value. But if your publisher can produce multiple values, it will not finish before all values has been emit.




回答7:


I was struggling with that problem too, and I finally found a solution. One thing to understand is that you cannot recover from a completed flux. The solution is to return a Result instead of an Error.

let button = UIButton()
button.publisher(for: .touchUpInside)
    .map({ control -> Result<String, Error> in
        if Bool.random() {
            return .failure(MyError.someError)
        } else {
            return .success("we're in the else case")
        }
    }).sink (receiveValue: { (result) in
        switch(result) {
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        }
    })

For anyone else reading this thread and trying to compile the code, you will need to import the code from this article (for the button.publisher(for: .touchUpIsinde) part).

Bonus, here's the code to handle errors with a PassthroughSubject and never complete the flux:

let subscriber = PassthroughSubject<Result<String, MyError>, Never>()

subscriber
    .sink(receiveValue: { result in
        switch result {
        case .success(let value):
            print("Received value: \(value)")
        case .failure(let error):
            print("Failure: \(String(describing: error))")
        }
    })

You cannot use PassthroughSubject<String, MyError>() directly, otherwise, the flux will complete when an error occurs.



来源:https://stackoverflow.com/questions/58675235/combine-how-to-replace-catch-an-error-without-completing-the-original-publisher

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!