问题
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