diff --git a/README.md b/README.md index e8249b0..b1c270c 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ This section outlines some of the custom operators CombineExt provides. ### withLatestFrom -Merges two publishers into a single publisher by combining each value from `self` with the _latest_ value from the second publisher, if any. +Merges up to four publishers into a single publisher by combining each value from `self` with the _latest_ value from the other publishers, if any. ```swift let taps = PassthroughSubject() diff --git a/Sources/Operators/WithLatestFrom.swift b/Sources/Operators/WithLatestFrom.swift index fc83638..f0b37ad 100644 --- a/Sources/Operators/WithLatestFrom.swift +++ b/Sources/Operators/WithLatestFrom.swift @@ -13,7 +13,7 @@ public extension Publisher { /// Merges two publishers into a single publisher by combining each value /// from self with the latest value from the second publisher, if any. /// - /// - parameter other: Second observable source. + /// - parameter other: A second publisher source. /// - parameter resultSelector: Function to invoke for each value from the self combined /// with the latest value from the second source, if any. /// @@ -22,19 +22,94 @@ public extension Publisher { /// specified result selector function. func withLatestFrom(_ other: Other, resultSelector: @escaping (Output, Other.Output) -> Result) - -> Publishers.WithLatestFrom { - return .init(upstream: self, second: other, resultSelector: resultSelector) + -> Publishers.WithLatestFrom { + return .init(upstream: self, second: other, resultSelector: resultSelector) + } + + + /// Merges three publishers into a single publisher by combining each value + /// from self with the latest value from the second and third publisher, if any. + /// + /// - parameter other: A second publisher source. + /// - parameter other1: A third publisher source. + /// - parameter resultSelector: Function to invoke for each value from the self combined + /// with the latest value from the second and third source, if any. + /// + /// - returns: A publisher containing the result of combining each value of the self + /// with the latest value from the second and third publisher, if any, using the + /// specified result selector function. + func withLatestFrom(_ other: Other, + _ other1: Other1, + resultSelector: @escaping (Output, (Other.Output, Other1.Output)) -> Result) + -> Publishers.WithLatestFrom, Result> + where Other.Failure == Failure, Other1.Failure == Failure { + let combined = other.combineLatest(other1) + .eraseToAnyPublisher() + return .init(upstream: self, second: combined, resultSelector: resultSelector) + } + + /// Merges four publishers into a single publisher by combining each value + /// from self with the latest value from the second, third and fourth publisher, if any. + /// + /// - parameter other: A second publisher source. + /// - parameter other1: A third publisher source. + /// - parameter other2: A fourth publisher source. + /// - parameter resultSelector: Function to invoke for each value from the self combined + /// with the latest value from the second, third and fourth source, if any. + /// + /// - returns: A publisher containing the result of combining each value of the self + /// with the latest value from the second, third and fourth publisher, if any, using the + /// specified result selector function. + func withLatestFrom(_ other: Other, + _ other1: Other1, + _ other2: Other2, + resultSelector: @escaping (Output, (Other.Output, Other1.Output, Other2.Output)) -> Result) + -> Publishers.WithLatestFrom, Result> + where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure { + let combined = other.combineLatest(other1, other2) + .eraseToAnyPublisher() + return .init(upstream: self, second: combined, resultSelector: resultSelector) } /// Upon an emission from self, emit the latest value from the /// second publisher, if any exists. /// - /// - parameter other: Second observable source. + /// - parameter other: A second publisher source. /// /// - returns: A publisher containing the latest value from the second publisher, if any. func withLatestFrom(_ other: Other) - -> Publishers.WithLatestFrom { - return .init(upstream: self, second: other) { $1 } + -> Publishers.WithLatestFrom { + return .init(upstream: self, second: other) { $1 } + } + + /// Upon an emission from self, emit the latest value from the + /// second and third publisher, if any exists. + /// + /// - parameter other: A second publisher source. + /// - parameter other1: A third publisher source. + /// + /// - returns: A publisher containing the latest value from the second and third publisher, if any. + func withLatestFrom(_ other: Other, + _ other1: Other1) + -> Publishers.WithLatestFrom, (Other.Output, Other1.Output)> + where Other.Failure == Failure, Other1.Failure == Failure { + withLatestFrom(other, other1) { $1 } + } + + /// Upon an emission from self, emit the latest value from the + /// second, third and forth publisher, if any exists. + /// + /// - parameter other: A second publisher source. + /// - parameter other1: A third publisher source. + /// - parameter other2: A forth publisher source. + /// + /// - returns: A publisher containing the latest value from the second, third and forth publisher, if any. + func withLatestFrom(_ other: Other, + _ other1: Other1, + _ other2: Other2) + -> Publishers.WithLatestFrom, (Other.Output, Other1.Output, Other2.Output)> + where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure { + withLatestFrom(other, other1, other2) { $1 } } } diff --git a/Tests/WithLatestFromTests.swift b/Tests/WithLatestFromTests.swift index e353682..a3612ab 100644 --- a/Tests/WithLatestFromTests.swift +++ b/Tests/WithLatestFromTests.swift @@ -104,7 +104,7 @@ class WithLatestFromTests: XCTestCase { var completed = false subscription = subject1 - .withLatestFrom(subject2) + .withLatestFrom(subject2) .sink(receiveCompletion: { _ in completed = true }, receiveValue: { results.append($0) }) @@ -135,4 +135,253 @@ class WithLatestFromTests: XCTestCase { XCTAssertTrue(completed) subscription.cancel() } + + func testWithLatestFrom2WithResultSelector() { + let subject1 = PassthroughSubject() + let subject2 = PassthroughSubject() + let subject3 = PassthroughSubject() + var results = [String]() + var completed = false + + subscription = subject1 + .withLatestFrom(subject2, subject3) { "\($0)|\($1.0)|\($1.1)" } + .sink( + receiveCompletion: { _ in completed = true }, + receiveValue: { results.append($0) } + ) + + subject1.send(1) + subject1.send(2) + subject1.send(3) + + subject2.send("bar") + + subject1.send(4) + subject1.send(5) + + subject3.send(true) + + subject1.send(10) + + subject2.send("foo") + + subject1.send(6) + + subject2.send("qux") + + subject3.send(false) + + subject1.send(7) + subject1.send(8) + subject1.send(9) + + XCTAssertEqual(results, ["10|bar|true", + "6|foo|true", + "7|qux|false", + "8|qux|false", + "9|qux|false" + ]) + + XCTAssertFalse(completed) + subject2.send(completion: .finished) + XCTAssertFalse(completed) + subject3.send(completion: .finished) + XCTAssertFalse(completed) + subject1.send(completion: .finished) + XCTAssertTrue(completed) + } + + func testWithLatestFrom2WithNoResultSelector() { + + struct Result: Equatable { + let string: String + let boolean: Bool + } + + let subject1 = PassthroughSubject() + let subject2 = PassthroughSubject() + let subject3 = PassthroughSubject() + var results = [Result]() + var completed = false + + subscription = subject1 + .withLatestFrom(subject2, subject3) + .sink( + receiveCompletion: { _ in completed = true }, + receiveValue: { results.append(Result(string: $0.0, boolean: $0.1)) } + ) + + subject1.send(1) + subject1.send(2) + subject1.send(3) + + subject2.send("bar") + + subject1.send(4) + subject1.send(5) + + subject3.send(true) + + subject1.send(10) + + subject2.send("foo") + + subject1.send(6) + + subject2.send("qux") + + subject3.send(false) + + subject1.send(7) + subject1.send(8) + subject1.send(9) + + XCTAssertEqual(results, [Result(string: "bar", boolean: true), + Result(string: "foo", boolean: true), + Result(string: "qux", boolean: false), + Result(string: "qux", boolean: false), + Result(string: "qux", boolean: false) + ]) + + XCTAssertFalse(completed) + subject2.send(completion: .finished) + XCTAssertFalse(completed) + subject3.send(completion: .finished) + XCTAssertFalse(completed) + subject1.send(completion: .finished) + XCTAssertTrue(completed) + } + + func testWithLatestFrom3WithResultSelector() { + let subject1 = PassthroughSubject() + let subject2 = PassthroughSubject() + let subject3 = PassthroughSubject() + let subject4 = PassthroughSubject() + + var results = [String]() + var completed = false + + subscription = subject1 + .withLatestFrom(subject2, subject3, subject4) { "\($0)|\($1.0)|\($1.1)|\($1.2)" } + .sink( + receiveCompletion: { _ in completed = true }, + receiveValue: { results.append($0) } + ) + + subject1.send(1) + subject1.send(2) + subject1.send(3) + + subject2.send("bar") + + subject1.send(4) + subject1.send(5) + + subject3.send(true) + subject4.send(5) + + subject1.send(10) + subject4.send(7) + + subject2.send("foo") + + subject1.send(6) + + subject2.send("qux") + + subject3.send(false) + + subject1.send(7) + subject1.send(8) + subject4.send(8) + subject3.send(true) + subject1.send(9) + + XCTAssertEqual(results, ["10|bar|true|5", + "6|foo|true|7", + "7|qux|false|7", + "8|qux|false|7", + "9|qux|true|8" + ]) + + XCTAssertFalse(completed) + subject2.send(completion: .finished) + XCTAssertFalse(completed) + subject3.send(completion: .finished) + XCTAssertFalse(completed) + subject4.send(completion: .finished) + XCTAssertFalse(completed) + subject1.send(completion: .finished) + XCTAssertTrue(completed) + } + + func testWithLatestFrom3WithNoResultSelector() { + + struct Result: Equatable { + let string: String + let boolean: Bool + let integer: Int + } + + let subject1 = PassthroughSubject() + let subject2 = PassthroughSubject() + let subject3 = PassthroughSubject() + let subject4 = PassthroughSubject() + + var results = [Result]() + var completed = false + + subscription = subject1 + .withLatestFrom(subject2, subject3, subject4) + .sink( + receiveCompletion: { _ in completed = true }, + receiveValue: { results.append(Result(string: $0.0, boolean: $0.1, integer: $0.2)) } + ) + + subject1.send(1) + subject1.send(2) + subject1.send(3) + + subject2.send("bar") + + subject1.send(4) + subject1.send(5) + + subject3.send(true) + subject4.send(5) + + subject1.send(10) + subject4.send(7) + + subject2.send("foo") + + subject1.send(6) + + subject2.send("qux") + + subject3.send(false) + + subject1.send(7) + subject1.send(8) + subject4.send(8) + subject3.send(true) + subject1.send(9) + + XCTAssertEqual(results, [Result(string: "bar", boolean: true, integer: 5), + Result(string: "foo", boolean: true, integer: 7), + Result(string: "qux", boolean: false, integer: 7), + Result(string: "qux", boolean: false, integer: 7), + Result(string: "qux", boolean: true, integer: 8) + ]) + + XCTAssertFalse(completed) + subject2.send(completion: .finished) + XCTAssertFalse(completed) + subject3.send(completion: .finished) + XCTAssertFalse(completed) + subject4.send(completion: .finished) + XCTAssertFalse(completed) + subject1.send(completion: .finished) + XCTAssertTrue(completed) + } }