Swift Combine:如何从发布者列表中创建单个发布者?

时间:2019-06-26 23:22:22

标签: swift combine

使用Apple的新的Combine框架,我希望从列表中的每个元素发出多个请求。然后,我希望减少所有响应得到一个结果。基本上,我想从发布者列表转到拥有响应列表的单个发布者。

我尝试制作发布者列表,但是我不知道如何将列表缩小为一个发布者。而且我尝试过使发布者包含一个列表,但是我无法平面映射发布者列表。

请查看“ 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>]
        }
    }

我不确定如何获取发布者数组并将其转换为包含数组的发布者。

类型“ [AnyPublisher]”的结果值与关闭结果类型“ Publisher”不符

4 个答案:

答案 0 :(得分:7)

我认为Publishers.MergeMany在这里可能会有所帮助。在您的示例中,您可能会这样使用它:

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

这将为您提供一个发布商,向您发送Output的单个值。

但是,如果您特别希望在所有发布者结束时一次将Output排列成一个数组,则可以将collect()MergeMany一起使用:

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

如果您愿意,可以将以上两个示例中的任何一个简化为一行:

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

您还可以在merge()上定义自己的自定义Sequence扩展方法,并使用它来稍微简化代码:

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

答案 1 :(得分:3)

本质上,在您的特定情况下,您正在看的是这样的东西:

    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(ingredients.count).eraseToAnyPublisher()
    }

这取决于您的每个内部发布商始终只产生一个准确的结果-这是需要注意的事情。

更通用的答案,您可以使用EntwineTest framework对其进行测试:

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` exactly three results (we know we're
        // waiting for as many results as we have original publishers), then
        // return the resulting publisher
        let finalPublisher = publisherOfPublishers.flatMap{ $0 }.collect(publishers.count)

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

答案 2 :(得分:1)

为了补充 Tricky 的答案,这里有一个保留数组中元素顺序的解决方案。 它为整个链中的每个元素传递一个索引,并按索引对收集到的数组进行排序。

由于排序,复杂度应该是 O(n log n)。

<div id= "search-data" class="form-search search-data" data-municipals="[&quot;Ajdovščina&quot;,&quot;Ankaran (Ancarano)&quot;,&quot;Apače&quot;,&quot;Beltinci&quot;,&quot;Benedikt&quot;,&quot;Bistrica ob Sotli&quot;,&quot;Bled&quot;,&quot;Bloke&quot;,&quot;Bohinj&quot;,&quot;Borovnica&quot;   [...] ;]">

<input type="text" name="q" id="q" autocomplete="off" class="form-control search-input" placeholder="Search by municipality..." />

解决方案的单元测试:

Storage::disk('public')->delete('post/adopsi/' . $adopsi->image_post_adps);

答案 3 :(得分:-2)

如果订单很重要,请尝试以下操作:

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