8
votes

Combiner: comment remplacer / rattraper une erreur sans compléter l'éditeur d'origine?

Compte tenu du code suivant:

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

Chaque fois que j'appuie sur le bouton, j'obtiens que we're in the else case Bool.random() jusqu'à Bool.random() que Bool.random() soit vrai - maintenant une erreur est généré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

    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)

au lieu de cela, je finished après l' replaced error et aucun événement n'est émis.

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


13 commentaires

Vous devez utiliser catch {}


mais quoi écrire dans le bloc Catch? Si j'utilise un Just, l'éditeur termine aussi wel


C'est une bonne question, ce que vous attendez est un nouvel éditeur qui est le même que celui actuel. Dans un cas courant, peut-être que «puits» n'est pas un abonné idéal ici. Essayez un sujet avant de couler


vous voulez dire un sujet personnalisé, c'est-à-dire un sujet qui "transmet" uniquement des valeurs et non des erreurs?


J'ai trouvé la réponse maintenant, utilisez simplement FlatMap, regardez les vidéos de la WWDC


avez-vous un lien vers le calendrier exact ou un petit exemple?


@swalkner comment avez-vous défini myButton?


c'est du storyboard: @IBOutlet var myButton: UIButton!


@swalkner J'ai publié ma réponse


Je vois ton problème maintenant. Vous devez diviser votre éditeur initial en deux parties. L'un est sur flatMap, sur flatMap d'initié. Par exemple, si vous pouvez produire un éditeur avec un paramètre, vous pouvez faire quelque chose comme ceci: Just (paramètre) .flatMap {(value) -> AnyPublisher <String, Never> in return MyPublisher (value) .catch {<String, Jamais> ()}} .sink (....) // MyPublisher (valeur) vous donnera un AnyPublisher <String, Error>. Catch peut le convertir en AnyPublisher <String, Never>. FlatMap continuera à produire de nouveaux éditeurs et ne finira jamais


@ E.Coms Je ne comprends pas, pouvez-vous mettre à jour votre réponse pour qu'elle soit formatée? 🙂


J'ai ajouté une nouvelle réponse comprenant également un lien vers le film WWDC'19 mentionné ci-dessus. N'hésitez pas à l'accepter comme la bonne réponse si vous pensez que cela vous aide.


Vous pouvez renvoyer votre propre erreur dans catch block: catch (let error) {return Fail (error: error) .receive (on: DispatchQueue.main) .eraseToAnyPublisher ()}


7 Réponses :


-1
votes

Insérez simplement flatMap comme suit et vous pouvez réaliser ce que vous voulez

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)

Le modèle de travail ressemble à ceci:

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

Si nous utilisons l'exemple ci-dessus, cela pourrait être comme ceci:

   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)

Ici, vous pouvez remplacer le firstPublisher par 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.


11 commentaires

ce n'est pas ce que je veux ... Je voudrais "effacer" l'erreur AVANT l'évier et APRÈS le tryMap. TryMap n'était utilisé que pour simuler un éditeur générant une erreur.


J'utilise également tryMap pour simuler l'erreur. Vous venez de mettre toute la logique try / catch ou try / replace ou catch dans le bloc flatmap, cela ne mettra pas fin à l'éditeur même après l'erreur de remplacement. Je pense que c'est ce dont vous avez besoin. Vous pouvez remplacer tryMap par n'importe quel autre.


mais vous avez l'erreur de lancer l'éditeur dans le flatMap , ce n'est pas ce que je recherche. Je cherche quelque chose où let something: AnyPublisher<String, Error> = ... peut être transformé en AnyPublisher<String, Never> - ou est-ce que je manque encore quelque chose?


FlatMap est un tel opérateur. Il empêche l'éditeur de résilier. ReplaceError peut également fonctionner, mais une fois qu'il corrige l'erreur, il mettra fin à l'éditeur. Vous avez donc besoin de cet opérateur intégré dans FlatMap. Ainsi, une fois l'erreur corrigée, l'éditeur d'origine sera repris. Vous pouvez exécuter le code pour voir si c'est le même que vous avez mentionné.


et si je n'ai PAS accès à l'éditeur d'origine (disons que c'est déjà un AnyPublisher<String, Error> ), comment puis-je l'envelopper dans flatMap?


Je vois. Vous voulez dire que vous n'avez pas d'éditeur d'UIButton?


Vous pouvez mettre votre éditeur dans FlatMap.


et quel est l'éditeur sur lequel flatMap s'appelle? L'avez-vous essayé?


Tout éditeur va bien avant FlatMap et insérez votre éditeur d'erreur dans FlatMap


ce n'est absolument pas ce que je veux. Je veux que cet éditeur se transforme, rien d'autre.


Actuellement, si vous utilisez votre code, une fois l'erreur remplacée, l'éditeur sera résilié et l'impression sera terminée. Eh bien, je crée moi-même un éditeur UIButton avec la propriété 'touchupinsder'. Puis accrochez-le à FlatMap. Il continue de fonctionner même s'il a l'erreur, il ne s'imprime jamais terminé. Le FlatMap est donc l'opérateur dont vous avez besoin ici.



0
votes

Je suggère d'utiliser Publisher avec typealias Failure = Never et la sortie en tant que résultat facultatif: typealias Output = Result<YourSuccessType, YourFailtureType>


0 commentaires

0
votes

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

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

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


4 commentaires

ici, je suis confronté au même problème. Dès que l'éditeur initial renvoie une erreur, la complétion est appelée et aucune valeur supplémentaire n'est émise.


Il y a une surcharge sur l'initialiseur pour Empty<String, Never>(completeImmediately: false) } qui l'empêche de se terminer, cela aiderait-il?


malheureusement, ce n'est pas le cas. Le bloc de complétion n'est pas appelé, mais aucune valeur n'est émise non plus.


La lecture de la documentation pour catch indique qu'elle remplace l'éditeur en amont, ce qui explique pourquoi plus aucune valeur n'est envoyée. Êtes-vous en mesure de renvoyer une nouvelle version de votre éditeur d'origine dans le catch plutôt qu'un Empty<String, Never> ? par exemple: .catch { _ in myButton.publisher(for: .touchUpInside)... }



-1
votes

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 d'un flux terminé. La solution consiste à renvoyer un Result au lieu d'une Error .

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

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

Bonus, voici le code pour gérer les erreurs avec un PassthroughSubject et ne jamais terminer le flux:

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

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


0 commentaires

4
votes

Je crois que la réponse d'E. Coms est correcte, mais je vais le dire beaucoup plus simple. La clé pour gérer les erreurs sans que le pipeline arrête le traitement des valeurs après une erreur est d'imbriquer votre éditeur de gestion des erreurs dans flatMap :

1
666
3
finished

Production:

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

Vous pouvez exécuter cet exemple dans une aire de jeux.


Concernant la modification du PO:

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

Vous ne pouvez pas.


0 commentaires

3
votes

Il y avait un film de la WWDC mentionné, et je crois que c'est "Combine in Practice" à partir de 2019, commencez à regarder vers 6h24: https://developer.apple.com/wwdc19/721

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

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

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

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 résilié par .catch , car c'est déjà un remplaçant et notre éditeur d'origine en amont est en sécurité. Cet exemple est tiré du film.

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

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


0 commentaires

0
votes

L'éditeur émet jusqu'à ce qu'il se termine ou échoue (avec une erreur), après quoi le flux sera arrêté.

Une façon de surmonter ce problème consiste à utiliser un résultat comme type d'éditeur

Éditeur

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)

Abonné

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


0 commentaires