Swift Combine: `append`, который не требует, чтобы вывод был равен? - PullRequest
0 голосов
/ 05 ноября 2019

Используя Apple Combine Я хотел бы добавить издателя bar после того, как первый издатель foo завершил (хорошо, чтобы ограничить Failure до Never). В основном я хочу RxJava andThen.

У меня есть что-то вроде этого:

let foo: AnyPublisher<Fruit, Never> = /* actual publisher irrelevant */

let bar: AnyPublisher<Fruit, Never> = /* actual publisher irrelevant */

// A want to do concatenate `bar` to start producing elements
// only after `foo` has `finished`, and let's say I only care about the
// first element of `foo`.
let fooThenBar = foo.first()
    .ignoreOutput()
    .append(bar) // Compilation error: `Cannot convert value of type 'AnyPublisher<Fruit, Never>' to expected argument type 'Publishers.IgnoreOutput<Upstream>.Output' (aka 'Never')`

Я нашел решение, я думаю, что оно работает, ноэто выглядит очень уродливо / чрезмерно сложно.

let fooThenBar = foo.first()
    .ignoreOutput()
    .flatMap { _ in Empty<Fruit, Never>() }
    .append(bar) 

Я что-то здесь упускаю?

Редактировать

В качестве ответа ниже добавлена ​​более приятная версия моего первоначального предложения. ,Большое спасибо @RobNapier!

Ответы [ 2 ]

1 голос
/ 05 ноября 2019

Я думаю, что вместо ignoreOutput, вы просто хотите отфильтровать все элементы, а затем добавить:

let fooThenBar = foo.first()
    .filter { _ in false }
    .append(bar)

Вы можете найти этот более хороший способ переименовать dropAll():

extension Publisher {
    func dropAll() -> Publishers.Filter<Self> { filter { _ in false } }
}

let fooThenBar = foo.first()
    .dropAll()
    .append(bar)

Основная проблема заключается в том, что ignoreAll() генерирует Publisher с выводом Never, что обычно имеет смысл. Но в этом случае вам нужно просто получить значения без изменения типа, и это фильтрация.

0 голосов
/ 06 ноября 2019

Благодаря большим дискуссиям с @RobNapier мы пришли к выводу, что решение flatMap { Empty }.append(otherPublisher) является лучшим, когда выход двух издателей различен. Поскольку я хотел использовать это после завершения работы первого издателя / base / 'foo', я написал расширение для Publishers.IgnoreOutput, в результате получилось следующее:

Solution

protocol BaseForAndThen {}
extension Publishers.IgnoreOutput: BaseForAndThen {}
extension Combine.Future: BaseForAndThen {}

extension Publisher where Self: BaseForAndThen, Self.Failure == Never {
    func andThen<Then>(_ thenPublisher: Then) -> AnyPublisher<Then.Output, Never> where Then: Publisher, Then.Failure == Failure {
        return
            flatMap { _ in Empty<Then.Output, Never>(completeImmediately: true) } // same as `init()`
                .append(thenPublisher)
                .eraseToAnyPublisher()
    }
}

Использование

В моем случае я хотел контролировать / иметь представление о , когда базовый издатель заканчивает работу, поэтому мое решение основано на этом.

Вместе с ignoreOutput

Поскольку второй издатель, в случае ниже appleSubject, не начнет производить элементы (выходные значения), пока не закончится первый издатель, я использую Оператор first() (есть также оператор last()) для завершения bananaSubject после одного выхода.

bananaSubject.first().ignoreOutput().andThen(appleSubject)

Вместе с Future

A Future уже просто производит один элемент, а затем завершает его.

futureBanana.andThen(applePublisher)

Test

Вот полный модульный тест ( также на Github )

import XCTest
import Combine

protocol Fruit {
    var price: Int { get }
}

typealias ? = Banana
struct Banana: Fruit {
    let price: Int
}

typealias ? = Apple
struct Apple: Fruit {
    let price: Int
}

final class CombineAppendDifferentOutputTests: XCTestCase {

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
    }

    func testFirst() throws {
        try doTest { bananaPublisher, applePublisher in
            bananaPublisher.first().ignoreOutput().andThen(applePublisher)
        }
    }

    func testFuture() throws {
        var cancellable: Cancellable?
        try doTest { bananaPublisher, applePublisher in

            let futureBanana = Future<?, Never> { promise in
                cancellable = bananaPublisher.sink(
                    receiveCompletion: { _ in },
                    receiveValue: { value in promise(.success(value)) }
                )
            }

            return futureBanana.andThen(applePublisher)
        }

        XCTAssertNotNil(cancellable)
    }

    static var allTests = [
        ("testFirst", testFirst),
        ("testFuture", testFuture),

    ]
}

private extension CombineAppendDifferentOutputTests {

    func doTest(_ line: UInt = #line, _ fooThenBarMethod: (AnyPublisher<?, Never>, AnyPublisher<?, Never>) -> AnyPublisher<?, Never>) throws {
        // GIVEN
        // Two publishers `foo` (?) and `bar` (?)
        let bananaSubject = PassthroughSubject<Banana, Never>()
        let appleSubject = PassthroughSubject<Apple, Never>()

        var outputtedFruits = [Fruit]()
        let expectation = XCTestExpectation(description: self.debugDescription)

        let cancellable = fooThenBarMethod(
            bananaSubject.eraseToAnyPublisher(),
            appleSubject.eraseToAnyPublisher()
            )
            .sink(
                receiveCompletion: { _ in expectation.fulfill() },
                receiveValue: { outputtedFruits.append($0 as Fruit) }
        )

        // WHEN
        // a send apples and bananas to the respective subjects and a `finish` completion to `appleSubject` (`bar`)
        appleSubject.send(?(price: 1))
        bananaSubject.send(?(price: 2))
        appleSubject.send(?(price: 3))
        bananaSubject.send(?(price: 4))
        appleSubject.send(?(price: 5))

        appleSubject.send(completion: .finished)

        wait(for: [expectation], timeout: 0.1)

        // THEN
        // A: I the output contains no banana (since the bananaSubject publisher's output is ignored)
        // and
        // B: Exactly two apples, more specifically the two last, since when the first Apple (with price 1) is sent, we have not yet received the first (needed and triggering) banana.
        let expectedFruitCount = 2
        XCTAssertEqual(outputtedFruits.count, expectedFruitCount, line: line)
        XCTAssertTrue(outputtedFruits.allSatisfy({ $0 is ? }), line: line)
        let apples = outputtedFruits.compactMap { $0 as? ? }
        XCTAssertEqual(apples.count, expectedFruitCount, line: line)
        let firstApple = try XCTUnwrap(apples.first)
        let lastApple = try XCTUnwrap(apples.last)
        XCTAssertEqual(firstApple.price, 3, line: line)
        XCTAssertEqual(lastApple.price, 5, line: line)
        XCTAssertNotNil(cancellable, line: line)
    }
}

...