Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

ios

swift

combine

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?

like image 667
swalkner Avatar asked Nov 02 '19 20:11

swalkner


People also ask

How do you catch errors in combine?

Catching errors If you want to catch errors early and ignore them after you can use the catch operator. This operator allows you to return a default value for if the request failed.

What is AnyPublisher in Swift?

AnyPublisher is a concrete implementation of Publisher that has no significant properties of its own, and passes through elements and completion values from its upstream publisher. Use AnyPublisher to wrap a publisher whose type has details you don't want to expose across API boundaries, such as different modules.

What is swift combine?

Overview. The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.


1 Answers

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)
}
like image 169
smat88dd Avatar answered Sep 18 '22 02:09

smat88dd