32 votes

Combine : comment remplacer/corriger une erreur sans compléter l'éditeur original ?

Étant donné le code suivant :

    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)

Chaque fois que j'appuie sur le bouton, j'obtiens we're in the else case jusqu'à Bool.random() est vrai - maintenant une erreur est déclenchée. J'ai essayé différentes choses, mais je n'ai pas réussi à attraper/remplacer/ignorer l'erreur et à continuer après avoir appuyé sur le bouton.

Dans l'exemple de code, j'aimerais avoir par exemple la sortie suivante

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

Au lieu de cela, j'obtiens finished après le replaced error et aucun événement n'est émis.

Modifier Étant donné un éditeur avec AnyPublisher<String, Error> comment puis-je le transformer en un AnyPublisher<String, Never> sans terminer lorsqu'une erreur se produit, c'est-à-dire ignorer les erreurs émises par l'éditeur original ?

34voto

smat88dd Points 53

Un film de la WWDC a été mentionné, et je crois qu'il s'agit de "Combine in Practice" de 2019, commencez à regarder vers 6:24 : https://developer.apple.com/wwdc19/721

Oui, .catch() met fin à l'éditeur en amont (film 7:45) et le remplace par un éditeur donné dans les arguments de la commande .catch ce qui se traduit généralement par .finished en cours de livraison lors de l'utilisation de Just() comme éditeur de remplacement.

Si l'éditeur d'origine doit continuer à travailler après un échec, une construction impliquant .flatMap() est nécessaire (film 9:34). L'opérateur entraînant un échec éventuel doit être exécuté dans le cadre de l'action .flatMap et peuvent y être traitées si nécessaire. L'astuce consiste à utiliser

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

au lieu de

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

À l'intérieur de .flatMap vous remplacez toujours l'éditeur et ne vous souciez donc pas de savoir si cet éditeur de remplacement est licencié par .catch puisqu'il s'agit déjà d'un remplacement et que notre éditeur original en amont est en sécurité. Cet exemple est tiré du film.

C'est aussi la réponse à votre Editar: question, sur la façon de transformer un <Output, Error> en <Output, Never> puisque le .flatMap ne produit pas d'erreurs, son Jamais avant et après le flatMap. Toutes les étapes liées aux erreurs sont encapsulées dans le flatMap. (Astuce pour vérifier si Failure=Never : si vous obtenez l'autocomplétion Xcode pour .assign(to:) alors je crois que vous avez un flux Failure=Never, cet abonné n'est pas disponible autrement. Et enfin le code complet du playground

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

11voto

Gil Birman Points 9000

Je pense que la réponse d'E. Coms est correcte, mais je vais la formuler de manière beaucoup plus simple. La clé pour gérer les erreurs sans que le pipeline n'arrête de traiter les valeurs après une erreur est d'imbriquer votre éditeur de gestion d'erreurs à l'intérieur des éléments suivants 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)
  })

Sortie :

1
666
3
finished

Vous pouvez exécuter cet exemple dans une cour de récréation.


En ce qui concerne l'édition de l'OP :

Modifier Étant donné un éditeur avec AnyPublisher<String, Error> comment puis-je le transformer en un AnyPublisher<String, Never> sans terminer lorsqu'une erreur se produit, c'est-à-dire ignorer les erreurs émises par l'éditeur original ?

Tu ne peux pas.

4voto

Pour ce faire, vous pouvez utiliser l'option catch opérateur et Empty éditeur :

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

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

0voto

E.Coms Points 3519

Il suffit d'insérer flatMap comme suit et vous pouvez obtenir ce que vous voulez.

   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)

Le modèle de travail ressemble à ceci :

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

Si nous reprenons l'exemple ci-dessus, cela pourrait être comme suit :

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)

Ici, vous pouvez remplacer le firstPublisher avec AnyPublisher qui prend un paramètre.

Ici aussi, le firstPublisher n'a qu'une seule valeur, il ne peut produire qu'une seule valeur. Mais si votre éditeur peut produire plusieurs valeurs, il ne se terminera pas avant que toutes les valeurs aient été émises.

0voto

Nicolas Bichon Points 9

Je me débattais aussi avec ce problème, et j'ai finalement trouvé une solution. Une chose à comprendre est que vous ne pouvez pas récupérer un flux terminé. La solution consiste à renvoyer un Result au lieu d'un 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))")
        }
    })

Pour toute autre personne lisant ce fil de discussion et essayant de compiler le code, vous devrez importer le code à partir de ceci article (pour le button.publisher(for: .touchUpIsinde) partie).

Bonus, voici le code pour gérer les erreurs avec un PassthroughSubject et de ne jamais terminer le 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))")
        }
    })

Vous ne pouvez pas utiliser PassthroughSubject<String, MyError>() directement, sinon, le flux se terminera par une erreur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X