Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 fours publishers into a single publisher by combining each value from `self` with the _latest_ value from the other publishers, if any.
Comment thread
freak4pc marked this conversation as resolved.
Outdated

```swift
let taps = PassthroughSubject<Void, Never>()
Expand Down
87 changes: 81 additions & 6 deletions Sources/Operators/WithLatestFrom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -22,19 +22,94 @@ public extension Publisher {
/// specified result selector function.
func withLatestFrom<Other: Publisher, Result>(_ other: Other,
resultSelector: @escaping (Output, Other.Output) -> Result)
-> Publishers.WithLatestFrom<Self, Other, Result> {
return .init(upstream: self, second: other, resultSelector: resultSelector)
-> Publishers.WithLatestFrom<Self, Other, Result> {
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: Publisher, Other1: Publisher, Result>(_ other: Other,
_ other1: Other1,
resultSelector: @escaping (Output, (Other.Output, Other1.Output)) -> Result)
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output), Self.Failure>, 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: Publisher, Other1: Publisher, Other2: Publisher, Result>(_ other: Other,
_ other1: Other1,
_ other2: Other2,
resultSelector: @escaping (Output, (Other.Output, Other1.Output, Other2.Output)) -> Result)
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output, Other2.Output), Self.Failure>, 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: Publisher>(_ other: Other)
-> Publishers.WithLatestFrom<Self, Other, Other.Output> {
return .init(upstream: self, second: other) { $1 }
-> Publishers.WithLatestFrom<Self, Other, Other.Output> {
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: Publisher, Other1: Publisher>(_ other: Other,
_ other1: Other1)
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output), Self.Failure>, (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: Publisher, Other1: Publisher, Other2: Publisher>(_ other: Other,
_ other1: Other1,
_ other2: Other2)
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output, Other2.Output), Self.Failure>, (Other.Output, Other1.Output, Other2.Output)>
where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure {
withLatestFrom(other, other1, other2) { $1 }
}
}

Expand Down
251 changes: 250 additions & 1 deletion Tests/WithLatestFromTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) })

Expand Down Expand Up @@ -135,4 +135,253 @@ class WithLatestFromTests: XCTestCase {
XCTAssertTrue(completed)
subscription.cancel()
}

func testWithLatestFrom2WithResultSelector() {
let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()
let subject3 = PassthroughSubject<Bool, Never>()
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<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()
let subject3 = PassthroughSubject<Bool, Never>()
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<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()
let subject3 = PassthroughSubject<Bool, Never>()
let subject4 = PassthroughSubject<Int, Never>()

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<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()
let subject3 = PassthroughSubject<Bool, Never>()
let subject4 = PassthroughSubject<Int, Never>()

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