22
votes

Swift Combine: comment créer un seul éditeur à partir d'une liste d'éditeurs?

En utilisant le nouveau framework Combine d'Apple, je souhaite effectuer plusieurs requêtes à partir de chaque élément d'une liste. Ensuite, je veux un seul résultat d'une réduction de toutes les réponses. Fondamentalement, je souhaite passer d'une liste d'éditeurs à un éditeur unique contenant une liste de réponses.

J'ai essayé de faire une liste d'éditeurs, mais je ne sais pas comment réduire cette liste en un seul éditeur. Et j'ai essayé de créer un éditeur contenant une liste, mais je ne peux pas mapper à plat une liste d'éditeurs.

Veuillez regarder la fonction "createIngredients"

func createIngredient(ingredient: Ingredient) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
    return apollo.performPub(mutation: CreateIngredientMutation(name: ingredient.name, optionalProduct: ingredient.productId, quantity: ingredient.quantity, unit: ingredient.unit))
            .eraseToAnyPublisher()
}

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
    // first attempt
    let results = ingredients
            .map(createIngredient)
    // results = [AnyPublisher<CreateIngredientMutation.Data, Error>]

    // second attempt
    return Publishers.Just(ingredients)
            .eraseToAnyPublisher()
            .flatMap { (list: [Ingredient]) -> Publisher<[CreateIngredientMutation.Data], Error> in
                return list.map(createIngredient) // [AnyPublisher<CreateIngredientMutation.Data, Error>]
            }
}

Je ne sais pas comment prendre un tableau d'éditeurs et le convertir en un éditeur contenant un tableau.

La valeur de résultat de type «[AnyPublisher]» n'est pas conforme au type de résultat de fermeture «Publisher»


2 commentaires

Si j'essaie d'utiliser eraseToAnyPublisher () avec apollo comme apollo.fetch(query: AllProductsQuery())).eraseToAnyPublisher() j'obtiens l'erreur La Value of type 'Cancellable' has no member 'eraseToAnyPublisher' - comment faites-vous sans voir une erreur?


@daidai J'ai utilisé une extension pour apollo pour accomplir cela. Cette question concerne en réalité la fusion de plusieurs éditeurs.


3 Réponses :


23
votes

Essentiellement, dans votre situation spécifique, vous envisagez quelque chose comme ceci:

import XCTest
import Combine
import EntwineTest

final class MyTests: XCTestCase {

    func testCreateArrayFromArrayOfPublishers() {

        typealias SimplePublisher = Publishers.Just<Int>

        // we'll create our 'list of publishers' here
        let publishers: [SimplePublisher] = [
            .init(1),
            .init(2),
            .init(3),
        ]

        // we'll turn our publishers into a sequence of
        // publishers, a publisher of publishers if you will
        let publisherOfPublishers = Publishers.Sequence<[SimplePublisher], Never>(sequence: publishers)

        // we flatten our publisher of publishers into a single merged stream
        // via `flatMap` then we `collect` all the results into a single array,
        // and finally we return the resulting publisher
        let finalPublisher = publisherOfPublishers.flatMap{ $0 }.collect()

        // Let's test what we expect to happen, will happen.
        // We'll create a scheduler to run our test on
        let testScheduler = TestScheduler()

        // Then we'll start a test. Our test will subscribe to our publisher
        // at a virtual time of 200, and cancel the subscription at 900
        let testableSubscriber = testScheduler.start { finalPublisher }

        // we're expecting that, immediately upon subscription, our results will
        // arrive. This is because we're using `just` type publishers which
        // dispatch their contents as soon as they're subscribed to
        XCTAssertEqual(testableSubscriber.sequence, [
            (200, .subscription),            // we're expecting to subscribe at 200
            (200, .input([1, 2, 3])),        // then receive an array of results immediately
            (200, .completion(.finished)),   // the `collect` operator finishes immediately after completion
        ])
    }
}

Cela «rassemble» tous les éléments produits par les éditeurs en amont et - une fois qu'ils ont tous terminé - produit un tableau avec tous les résultats et se complète enfin.

Gardez à l'esprit que si l'un des éditeurs en amont échoue - ou produit plus d'un résultat - le nombre d'éléments peut ne pas correspondre au nombre d'abonnés, vous pouvez donc avoir besoin d'opérateurs supplémentaires pour atténuer cela en fonction de votre situation.

La réponse la plus générique, avec un moyen de la tester à l'aide du framework EntwineTest :

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
    let publisherOfPublishers = Publishers.Sequence<[AnyPublisher<CreateIngredientMutation.Data, Error>], Error>(sequence: ingredients.map(createIngredient))
    return publisherOfPublishers.flatMap { $0 }.collect().eraseToAnyPublisher()
}


3 commentaires

Il convient de noter que cela ne préserve pas l'ordre du tableau sous-jacent. Les éléments du tableau final seront triés dans l'ordre que chaque éditeur a terminé.


@rpowell des idées sur la façon dont vous pourriez conserver la commande?


Je n'ai pas trouvé de bonne solution pour ça. Je trie actuellement mes valeurs une fois qu'elles ont toutes été récupérées.



-2
votes

Essayez quelque chose comme ceci si l'ordre est important:

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
    // first attempt
    let results = ingredients
            .map(createIngredient)
    // results = [AnyPublisher<CreateIngredientMutation.Data, Error>]

    var resultPublisher = Empty<CreateIngredientMutation.Data, Error>

    for result in results {
        resultPublisher = resultPublisher.append(result)
    }

    return resultPublisher.collect()
}


1 commentaires

OP travaille avec un éventail d'éditeurs - pas un seul éditeur.



7
votes

Je pense que Publishers.MergeMany pourrait être utile ici. Dans votre exemple, vous pouvez l'utiliser comme ceci:

extension Sequence where Element: Publisher {
    func merge() -> Publishers.MergeMany<Element> {
        Publishers.MergeMany(self)
    }
}

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
    ingredients.map(createIngredient).merge().eraseToAnyPublisher()
}

Cela vous donnera un éditeur qui vous enverra des valeurs uniques de la Output .

Cependant, si vous voulez spécifiquement la Output dans un tableau à la fois à la fin de tous vos éditeurs, vous pouvez utiliser collect() avec MergeMany :

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
    Publishers.MergeMany(ingredients.map(createIngredient(ingredient:))).eraseToAnyPublisher()
}

Et l'un ou l'autre des exemples ci-dessus, vous pouvez simplifier en une seule ligne si vous préférez, c'est-à-dire:

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
    let publishers = ingredients.map(createIngredient(ingredient:))
    return Publishers.MergeMany(publishers).collect().eraseToAnyPublisher()
}

Vous pouvez également définir votre propre méthode d'extension personnalisée merge() sur Sequence et l'utiliser pour simplifier légèrement le code:

func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
    let publishers = ingredients.map(createIngredient(ingredient:))
    return Publishers.MergeMany(publishers).eraseToAnyPublisher()
}


0 commentaires