Chaining array of AnyPublishers - swift

I'm wondering if there is a way to chain array of publishers similar to how we chain publishers with regular flatMap
Let's say I have three publishers: publisher1, publisher2, publisher3 all of them have the same Output, Failure types. For example, each of them is AnyPublisher<String, Never> and emits a single String value. The only role of each publisher is to fetch its own value and emits previous value joined with its own.
I'm looking for same effect as from the following pseudo code:
let pipe = publisher1(value: "")
.flatMap { publisher2(value: $0) }
.flatMap { publisher3(value: $0) }
Execution flow:
publisher1 (fetches "A") -> publisher2 (fetches "B") -> publisher3 (fetches "C") -> "ABC"
I would like to reproduce the same flow for the array with unknown count of publishers n ([AnyPublisher<String, Never>])
1 -> 2 -> 3 -> ... -> n
I'll appreciate any tips, thanks! :)

First of all, let's clarify your question. Based on your description of wanting to create a chain of flatMap-ed publishers, what you must have is an array of closures - not publishers - each returning an AnyPublisher<String, Never> publisher given a String parameter.
let pubs: [(String) -> AnyPublisher<String, Never>] = [
publisher1, publisher2, publisher3
]
To chain them, you could use a reduce method of the array by starting with a Just publisher to emit the initial parameter:
let pipe = pubs.reduce(Just("").eraseToAnyPublisher()) { acc, next in
acc.flatMap { next($0) }.eraseToAnyPublisher()
}

If I understand you correctly you should be able to use append on your publishers
let pub1: AnyPublisher<String, Never> = ["A1", "B1", "C1"].publisher.eraseToAnyPublisher()
let pub2: AnyPublisher<String, Never> = ["A2", "B2", "C2"].publisher.eraseToAnyPublisher()
let pub3: AnyPublisher<String, Never> = ["A3", "B3", "C3"].publisher.eraseToAnyPublisher()
_ = pub1.append(pub2.append(pub3))
.sink(receiveValue: { value in
print(value)
})

Another approach is to zip the publishers together, and them combine the latest values:
let publisher1 = ["A"].publisher
let publisher2 = ["B"].publisher
let publisher3 = ["C"].publisher
_ = publisher1.zip(publisher2, publisher3)
.map { $0+$1+$2 }
.sink(receiveValue: { print("Combined: \($0)") })
/// prints ABC
Or, if you have a variable size number of publishers, you can use MergeMany and reduce:
// same result: ABC
_ = Publishers.MergeMany([publisher1, publisher2, publisher3])
.reduce("") { $0 + $1 }
.sink(receiveValue: { print("Combined: \($0)") })
You can go even further, and write your own publisher, if you think you'll be needing this functionality in multiple places:
extension Publishers {
/// works also with arrays, or any other range replaceable collection
struct ConcatenateOutputs<Upstream: Publisher> : Publisher where Upstream.Output: RangeReplaceableCollection {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
private let reducer: AnyPublisher<Upstream.Output, Failure>
init(_ publishers: Upstream...) {
self.init(publishers)
}
init<S: Swift.Sequence>(_ publishers: S) where S.Element == Upstream {
reducer = MergeMany(publishers)
.reduce(Output.init()) { $0 + $1 }
.eraseToAnyPublisher()
}
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
reducer.receive(subscriber: subscriber)
}
}
}
extension Sequence where Element: Publisher, Element.Output: RangeReplaceableCollection {
var concatenateOutputs: Publishers.ConcatenateOutputs<Element> { .init(self) }
}
// same output
_ = Publishers.ConcatenateOutputs([publisher1, publisher2, publisher3])
.sink(receiveValue: { print("Combined: \($0)") })
// the variadic initializer works the same
_ = Publishers.ConcatenateOutputs(publisher1, publisher2, publisher3)
.sink(receiveValue: { print("Combined: \($0)") })
// the less verbose construct
_ = [publisher1, publisher2, publisher3].concatenateOutputs
.sink(receiveValue: { print("Combined: \($0)") })

Related

Combine - bind a stream into another and handle side effects while doing it

I am trying to learn Combine. I know the terms and the basic concept theoretically. But when trying to work with it, I am lost.
I am trying to do is map an Input stream of events to Output stream of state. Is there a way to bind the result of the map to outputSubject? I am trying to make it work with sink but is there a better way?
Also is there an operator equivalent of RxSwift's withLatestFrom?
import Combine
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
mapEventToState(event, with: outputSubject.value)
}
.handleOutput { state in
handleSideEffects(for: state) // Also, how do I access the event here if I needed?
}
.sink {
outputSubject.send($0)
}
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}
Instead of using sink to assign a value to a CurrentValueSubject, I would use assign.
If you want to do something with the values in the middle of a pipeline you can use the handleEvents operator, though if you look in the documentation you'll see that the operator is listed as a debugging operator because generally your pipeline should not have side effects (building it from pure functions is one of the primary benefits.
Just reading the description of withLatestFrom in the RX documentation, I think the equivalent in combine is combineLatest
Here's your code, put into a Playground, and modified a bit to illustrates the first two points:
import Combine
struct Event {
var placeholder: String
}
enum State {
case initial
}
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
self.mapEventToState(event, with: self.outputSubject.value)
}
.handleEvents(receiveOutput: { value in
debugPrint("Do something with \(value)")
})
.assign(to: \.outputSubject.value, on: self)
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
return .initial
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}

Create a publisher that emits a value on completion of another publisher

I have a publisher that never emits items and only completes or fails with an error (AnyPublisher<Never, Error>). I want to transform that publisher into a publisher that emits a value when the first publisher completes (AnyPublisher<Value, Error>), or passes through any error. I want to create that value after completion of the first publisher. I could do something like this, but it seems quite messy:
func demo() -> AnyPublisher<Int, Error> {
// Using Empty just for demo purposes
let firstPublisher = Empty<Never, Error>(completeImmediately: true).eraseToAnyPublisher()
var cancellable: AnyCancellable?
return Future<Int, Error> { promise in
cancellable = firstPublisher
.sink { completion in
switch completion {
case .failure(let error):
promise(.failure(error))
case .finished:
// some operation that generates value
let value:Int = 1
promise(.success(value))
}
} receiveValue: { _ in
}
}
.handleEvents(receiveCancel: {
cancellable?.cancel()
})
.eraseToAnyPublisher()
}
Can this be done a better way? Something like:
extension AnyPublisher {
func completionMap<T, P>(_: (_ completion: Subscribers.Completion<Self.Failure>) -> P) -> P where P: Publisher, T == P.Output, Self.Failure == P.Failure {
/// ???
}
}
func demo() -> AnyPublisher<Int, Error> {
// Using Empty just for demo purposes
let firstPublisher = Empty<Never, Error>(completeImmediately: true).eraseToAnyPublisher()
return firstPublisher
.completionMap { completion -> AnyPublisher<Int, Error> in
switch completion {
case .failure(let error):
return Fail(error: error).eraseToAnyPublisher()
case .finished:
// some operation that generates value
let value:Int = 1
return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
You could use .append (which returns a Publishers.Concatenate) as a way to emit a value after the first publisher completes.
let firstPublisher: AnyPublisher<Never, Error> = ...
let demo = firstPublisher
.map { _ -> Int in }
.append([1])
The above will emit 1 if firstPublisher completes successfully, or would error out.

How to best create a publisher aggregate of #Published values in Combine?

Given an hierarchical structure of #OberservableObjects - I often find myself in a situation where I need a publisher which provides some kind of updated aggregate of the entire structure (the example below calculates a sum, but it could be anything)
Below is the solution I have come up with - which kinda works, but also not... :)
Problem #1: It looks way to complicated - and I feel I am missing something...
Problem #2: It does not work as the $foo publisher on top does emit changes to foo before foo changes, which are then not present in the second self.$foo publisher (which shows the old state).
Sometimes I need the aggregate in sync with swiftUI view updates - so that I need to utilize the #Published value and no separate publisher that emits during didSet of the variable.
I did not find a good solution... So how would you guys resolve this?
class Foo:ObservableObject {
#Published var bar:Int = 0
}
class Foobar:ObservableObject {
#Published var foo:[Foo] = []
var sumPublisher:AnyPublisher<Int,Never> {
// Whenever the foo array or one of the foo.bar values change
//
$foo
.map { fooArray in
Publishers.MergeMany( fooArray.map { foo in foo.$bar } )
}
.switchToLatest()
// Calclulate a new sum by collecting and reducing all foo.bar values.
//
.map { [unowned self] _ in
self.$foo // <--- in case of a foo change, this is still the unchanged foo, therefore not correct.
.map { fooArray -> AnyPublisher<Int,Never> in
Publishers.MergeMany( fooArray.map { foo in foo.$bar.first() } )
.collect()
.map { barArray -> Int in
barArray.reduce(0, { $0 + $1 })
}
.eraseToAnyPublisher()
}
.switchToLatest()
}
.switchToLatest()
.removeDuplicates()
.eraseToAnyPublisher()
}
}
Problem #2 : #Published fire signals on "willSet" and not "didSet".
You can use this extension :
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
and
self.$foo.didSet
.map { _ in
//...//
}
Problem #1 :
Maybe so :
class Foobar:ObservableObject {
#Published var foo:[Foo] = []
#Published var sum = 0
var cancellable: AnyCancellable?
init() {
cancellable =
sumPublisher
.sink {
self.sum = $0
}
}
var sumPublisher: AnyPublisher<Int,Never> {
let firstPublisher = $foo.didSet
.flatMap {array in
array.publisher
.flatMap { $0.$bar.didSet }
.map { _ -> [Foo] in
return self.foo
}
}
.eraseToAnyPublisher()
let secondPublisher = $foo.didSet
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map { barArray -> Int in
return barArray
.map {$0.bar}
.reduce(0, { $0 + $1 })
}
.removeDuplicates()
.eraseToAnyPublisher()
}
}
And to test :
struct FooBarView: View {
#StateObject var fooBar = Foobar()
var body: some View {
VStack {
HStack {
Button("Change list") {
fooBar.foo = (1 ... Int.random(in: 5 ... 9)).map { _ in Int.random(in: 1 ... 9) }.map(Foo.init)
}
Text(fooBar.sum.description)
Button("Change element") {
let idx = Int.random(in: 0 ..< fooBar.foo.count)
fooBar.foo[idx].bar = Int.random(in: 1 ... 9)
}
}
List(fooBar.foo, id: \.bar) { foo in
Text(foo.bar.description)
}
.onAppear {
fooBar.foo = [1, 2, 3, 8].map(Foo.init)
}
}
}
}
EDIT :
If you really prefer to use #Published (the willSet publisher), it sends the new value of bar therefore you could deduce the new value of foo (the array) :
var sumPublisher: AnyPublisher<Int, Never> {
let firstPublisher = $foo
.flatMap { array in
array.enumerated().publisher
.flatMap { index, value in
value.$bar
.map { (index, $0) }
}
.map { index, value -> [Foo] in
var newArray = array
newArray[index] = Foo(bar: value)
return newArray
}
}
.eraseToAnyPublisher()
let secondPublisher = $foo
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map { barArray -> Int in
barArray
.map { $0.bar }
.reduce(0, { $0 + $1 })
}
.removeDuplicates()
.eraseToAnyPublisher()
}
By far, the easiest approach here is to use a struct instead of a class with #Published:
struct Foo {
var bar: Int = 0
}
Then you can simply create a computed property:
class Foobar: ObservableObject {
#Published var foo: [Foo] = []
var sum: Int {
foo.map(\.bar).reduce(0, +)
}
// ...
}
For SwiftUI views, you wouldn't even need to make it a Publisher - when foo changes, because it's #Published, it will cause the View to access sum again, which would give it the recomputed value.
If you insist on it being a Publisher, it's still easy to do, since foo itself changes when any of its values Foo change (since they are value-type structs):
var sumPublisher: AnyPublisher<Int, Never> {
self.$foo
.map { $0.map(\.bar).reduce(0, +) }
.eraseToAnyPublisher()
}
Sometimes, it's not possible to change a class into a struct for whatever reason (maybe each class has its own life cycle that self-updates). Then you'd need to manually keep track of all the additions/removals of Foo objects in the array (via willSet or didSet), and subscribe to changes in their foo.bar.

how to use Publishers.CombineLatest to get 1 publisher

I am trying to use 2 publishers and have them stream to 1 publisher that is mapped from both values.
My code is:
class ViewModel {
let email = CurrentValueSubject<String, Never>("")
lazy var isEmailValid = email.map { self.validateEmail(email: $0) }
let password = CurrentValueSubject<String, Never>("")
lazy var isPasswordCorrect = password.map {
self.validatePassword(password: $0)
}
let canLogin: CurrentValueSubject<Bool, Never>
private func validateEmail(email: String) -> Bool {
return email == "1234#gmail.com"
}
private func validatePassword(password: String) -> Bool {
return password == "1234"
}
init() {
canLogin = Publishers
.CombineLatest(isEmailValid, isPasswordCorrect)
.map { $0 && $1 }
}
}
Then in the init I get this error:
//error: Cannot assign value of type
'Publishers.Map<Publishers.CombineLatest<Publishers.Map<CurrentValueSubject<String, Never>,
Bool>, Publishers.Map<CurrentValueSubject<String, Never>, Bool>>, Bool>' to type 'CurrentValueSubject<Bool, Never>'
I am new to combine so I find it a little confusing.
How should I achieve, from the code above, the combination of 2 publishers isEmailValid and isPasswordCorrect, into 1 publisher that is a CurrentValueSubject<Bool, Never>?
A CurrentValueSubject is:
A subject that wraps a single value and publishes a new element whenever the value changes.
Your canLogin is certainly not a CurrentValueSubject. It is the result of combining two other publishers with the CombineLatest operator, and then mapping the combined publisher to yet another publisher.
In the language of the Swift type system, this kind of publisher is called:
Publishers.Map<Publishers.CombineLatest<Publishers.Map<CurrentValueSubject<String, Never>, Bool>, Publishers.Map<CurrentValueSubject<String, Never>, Bool>>, Bool>
Obviously, no one would declare a property with a type like that, so we use eraseToAnyPublisher to get ourselves an AnyPublisher, to say that we don't actually care what type of publisher it is.
let canLogin: AnyPublisher<Bool, Never>
...
canLogin = Publishers
.CombineLatest(isEmailValid, isPasswordCorrect)
.map { $0 && $1 }
.eraseToAnyPublisher()
You've declared the type of canLogin completely incorrectly.
It needs to be an AnyPublisher, which you can get by simply calling eraseToAnyPublisher on the map.
lazy var canLogin: AnyPublisher<Bool, Never> = isEmailValid.combineLatest(isPasswordCorrect).map { $0 && $1 }.eraseToAnyPublisher()

Swift Combine Chunk Operator

I'm trying to create chunks of a stream in Apple's Combine framework.
What I'm going for is something like this:
Stream a:
--1-2-3-----4-5--->
Stream b:
--------0-------0->
a.chunk(whenOutputFrom: b)
-------[1, 2, 3]---[4, 5]-->
Can this be implemented in Combine?
What you are looking for is the buffer operator in the ReactiveX world.
There is no built in buffer operator (in the ReactiveX sense) in Combine. The built-in buffer is seems to be more like a bufferCount in ReactiveX.
I found this answer by Daniel T, which recreates the buffer operator in RxSwift, and also this cheatsheet, which tells you how to port RxSwift to Combine.
However, the answer by Daniel T uses Observable.create, which isn't available in Combine. I looked a bit deeper, and found this other answer, that recreates Observable.create in Combine.
Combining the three things I've found (no pun intended), this is what I came up with:
// -------------------------------------------------
// from https://stackoverflow.com/a/61035663/5133585
struct AnyObserver<Output, Failure: Error> {
let onNext: ((Output) -> Void)
let onError: ((Failure) -> Void)
let onCompleted: (() -> Void)
}
struct Disposable {
let dispose: () -> Void
}
extension AnyPublisher {
static func create(subscribe: #escaping (AnyObserver<Output, Failure>) -> Disposable) -> Self {
let subject = PassthroughSubject<Output, Failure>()
var disposable: Disposable?
return subject
.handleEvents(receiveSubscription: { subscription in
disposable = subscribe(AnyObserver(
onNext: { output in subject.send(output) },
onError: { failure in subject.send(completion: .failure(failure)) },
onCompleted: { subject.send(completion: .finished) }
))
}, receiveCancel: { disposable?.dispose() })
.eraseToAnyPublisher()
}
}
// -------------------------------------------------
// -------------------------------------------------
// adapted from https://stackoverflow.com/a/43413167/5133585
extension Publisher {
/// collects elements from the source sequence until the boundary sequence fires. Then it emits the elements as an array and begins collecting again.
func buffer<T: Publisher, U>(_ boundary: T) -> AnyPublisher<[Output], Failure> where T.Output == U {
return AnyPublisher.create { observer in
var buffer: [Output] = []
let lock = NSRecursiveLock()
let boundaryDisposable = boundary.sink(receiveCompletion: {
_ in
}, receiveValue: {_ in
lock.lock(); defer { lock.unlock() }
observer.onNext(buffer)
buffer = []
})
let disposable = self.sink(receiveCompletion: { (event) in
lock.lock(); defer { lock.unlock() }
switch event {
case .finished:
observer.onNext(buffer)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
buffer = []
}
}) { (element) in
lock.lock(); defer { lock.unlock() }
buffer.append(element)
}
return Disposable {
disposable.cancel()
boundaryDisposable.cancel()
}
}
}
}
// -------------------------------------------------
I think you would be interested in Combine collect() method.
there is variation it it as well such as by time, count or both.
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
where we pass context -> for example global queue
time to wait for it such as 1s in above example
and the count of 10 elements.
Use case would look something like this:
let bufferSubject = PassthroughSubject<Int, Never>()
let cancelBag = Set<AnyCancellable>()
let subscriber = bufferSubject.eraseToAnyPublisher()
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
.sink { value in
print("🚀🚀 value: \(value)")
}
.store(in: &cancelBag)
be sure to test it :)
bufferSubject.send(1)
bufferSubject.send(2)
bufferSubject.send(3)
...
DispatchQueue.asyncAfter(...) {
bufferSubject.send(4)
bufferSubject.send(5)
bufferSubject.send(6)
}