Swift Combine: `append` which does not require output to be equal?

随声附和 提交于 2020-03-04 16:01:10

问题


Using Apple's Combine I would like to append a publisher bar after a first publisher foo has finished (ok to constrain Failure to Never). Basically I want RxJava's andThen.

I have something like this:

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')`

I've come up with a solution, I think it works, but it looks very ugly/overly complicated.

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

I'm I missing something here?

Edit

Added a nicer version of my initial proposal as an answer below. Big thanks to @RobNapier!


回答1:


I think instead of ignoreOutput, you just want to filter all the items, and then append:

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

You may find this nicer to rename dropAll():

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

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

The underlying issue is that ignoreAll() generates a Publisher with Output of Never, which usually makes sense. But in this case you want to just get ride of values without changing the type, and that's filtering.




回答2:


Thanks to great discussions with @RobNapier we kind of concluded that a flatMap { Empty }.append(otherPublisher) solution is the best when the output of the two publishers differ. Since I wanted to use this after the first/base/'foo' publisher finishes, I've written an extension on Publishers.IgnoreOutput, the result is this:

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

Usage

In my use case I wanted to control/have insight in when the base publisher finishes, therefore my solution is based on this.

Together with ignoreOutput

Since the second publisher, in case below appleSubject, won't start producing elements (outputting values) until the first publisher finishes, I use first() operator (there is also a last() operator) to make the bananaSubject finish after one output.

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

Together with Future

A Future already just produces one element and then finishes.

futureBanana.andThen(applePublisher)

Test

Here is the complete unit test (also on 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)
    }
}



来源:https://stackoverflow.com/questions/58718134/swift-combine-append-which-does-not-require-output-to-be-equal

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!