PassthroughSubject + flatMap cannot be called more than once? - swift

If I send to PassthroughSubject<Void, Error>, this process will run once, but if I send it more than once, the process in the flatMap will not run. Why is this?
var testTappedSubject = PassthroughSubject<Void, Error>()
testTappedSubject
.print("testTappedSubject")
.flatMap({ () -> AnyPublisher<Int, Error> in
print("called test")
return Fail(error: LoginError.someError(error: .unknown))
.eraseToAnyPublisher()
})
.sink { error in
print("error", error)
} receiveValue: { value in
print("pressed", value)
}
.store(in: &cancellables)

That's the way Combine (and other Reactive frameworks) work.
you are setting up a subscription to a publisher, and the only thing that subscription does is emit an error:
return Fail(error: LoginError.someError(error: .unknown))
.eraseToAnyPublisher()
When a publisher emits an error. the subscription completes and does not receive any more events.
I put a version of your code in a playground:
import Combine
var cancellables = Set<AnyCancellable>()
var testTappedSubject = PassthroughSubject<Void, Error>()
enum LoginError: Error {
case unknown
}
testTappedSubject
.print("testTappedSubject")
.flatMap({ () -> AnyPublisher<Int, Error> in
print("called test")
return Fail(error: LoginError.unknown)
.eraseToAnyPublisher()
})
.sink { error in
print("error", error)
} receiveValue: { value in
print("pressed", value)
}
.store(in: &cancellables)
testTappedSubject.send()
testTappedSubject.send()
testTappedSubject.send()
and the results in the console were:
testTappedSubject: receive subscription: (PassthroughSubject)
testTappedSubject: request unlimited
testTappedSubject: receive value: (())
called test
error failure(__lldb_expr_72.LoginError.unknown)
testTappedSubject: receive value: (())
testTappedSubject: receive value: (())
This shows that an unlimited subscription was received, and then, after sending a value, your print("called test") is called and then an error is received.
The subscription is now complete.
Sending more values just shows that a value was received, but nothing was sent to the subscriber.

Related

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.

Combine: difference between Just and first() to finish a stream

I'm new to Combine and I don't understand the behaviour in these cases:
func getPublisherWithFirst() -> AnyPublisher<Value, Error> {
// somePublisher: PassthroughSubject<Bool, Never>
return somePublisher
.compactMap { (someBool) -> Value? in
if someBool {
return Value()
}
return nil
}
.setFailureType(to: Error.self)
.first()
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func getPublisherWithFlatMap() -> AnyPublisher<Value, Error> {
// somePublisher: PassthroughSubject<Bool, Never>
return somePublisher
.flatMap { (someBool) -> AnyPublisher<Value, Never> in
if someBool {
return Just(Value()).eraseToAnyPublisher()
}
return Empty(completeImmediately: false, outputType: Value.self, failureType: Never.self).eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
When using getPublisherWithFlatMap, my stream does not finish (the timeout is triggered), even if a Just publisher is returned and it is supposed to finish. Why?
The "issue" is caused by you using a PassthroughSubject as your upstream. PassthroughSubject never completes unless you explicitly send a completion on it.
When using first, it doesn't matter if the upstream completes or not, since first completes as soon as its upstream emits a single value. However, in case of flatMap, flatMap executes its closure on every single value emitted by its upstream and hence it only completes when its upstream completes. It doesn't matter that you return a Just inside your flatMap, that Just will only be returned for a single value, not for the upstream as a whole.
If you want to ensure that getPublisherWithFlatMap also completes after emitting a single value, you should call first on it as well, similarly to getPublisherWithFirst.
func getPublisherWithFlatMap() -> AnyPublisher<Value, Error> {
// somePublisher: PassthroughSubject<Bool, Never>
return somePublisher
.flatMap { (someBool) -> AnyPublisher<Value, Never> in
if someBool {
return Just(Value()).eraseToAnyPublisher()
}
return Empty(completeImmediately: false, outputType: Value.self, failureType: Never.self).eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
.first()
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}

Why .collect() operator in swift Combine always sends .unlimited demand regardless of the demand of upstream publisher?

I have been playing with Combine to understand how it works in more details and I create a custom Publisher, Subscription and Subscriber.
Here's how it looks..
The emoji beamer publisher along with subscription:
struct EmojiBeamerPublisher: Publisher {
typealias Output = String
typealias Failure = Error
private let emojis: [String] = ["👍","❤️","✅","🥰","😍","🚀","😅","🍑","🍞","🎅","❄️","🐻","👀","👄","🦷","✍️","🙏","👨‍💻","🐝","🐛","🦉","🦀","🐍","🐞","🧸"]
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = EmojiBeamerSubscription(output: emojis, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
extension EmojiBeamerPublisher {
private final class EmojiBeamerSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == Failure {
var subscriber: S?
let output: [String]
init(output: [String], subscriber: S) {
self.subscriber = subscriber
self.output = output
}
func request(_ demand: Subscribers.Demand) {
Swift.print("Demand: \(demand)") // Here I receive Unlimited demand
var demand = demand
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] timer in
guard let self = self else { return }
guard demand > 0, let subscriber = self.subscriber else {
timer.invalidate()
self.subscriber?.receive(completion: .finished)
self.cancel()
return
}
demand -= 1
demand += subscriber.receive(self.output.randomElement()! + " \(Date())")
}
}
func cancel() {
subscriber = nil
}
}
}
Here is my Custom subscriber:
final class EmojiBeamerSubscriber<Input, Failure: Error>: Subscriber, Cancellable {
var subscription: Subscription?
let receiveValue: (Input) -> Void
init(receiveValue: #escaping (Input) -> Void) {
self.receiveValue = receiveValue
}
func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(3)) // Here I send only 3 as max demand
}
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
print("Will handle later:", completion)
}
func cancel() {
self.subscription?.cancel()
self.subscription = nil
}
}
extension Publisher {
func myCustomSink(receiveValueHandler: #escaping (Self.Output) -> Void) -> AnyCancellable {
let myCustomSubscriber = EmojiBeamerSubscriber<Self.Output, Self.Failure>(receiveValue: receiveValueHandler)
subscribe(myCustomSubscriber)
return AnyCancellable(myCustomSubscriber)
}
}
As you can see on my custom subscription I request with demand .max(3) if I don't use collect everything works fine, I get an emoji beamed every 5 second after 3 I got a .finish completion.
Works fine (and sends .max(3) demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
However if I simply add .collect() to catch all 3 results at once in an array it just requests with .unlimited demand on my subscription, resulting in a never ending subscription because my demand will never reach zero.
Never complete (and sends unlimited demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.collect()
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
Is there something wrong with my implementation? Or Did I misunderstood the purpose of .collect() operator?
Thank you in advance :)
From the documentation:
This publisher requests an unlimited number of elements from the upstream publisher and uses an unbounded amount of memory to store the received values. The publisher may exert memory pressure on the system for very large sets of elements.
So the behaviour you noticed is the correct one, as collect() sends an unlimited demand upstream.
The unlimited demand causes the demand -= 1 instruction to do nothing, so the demand > 0 check will always pass, resulting into an infinite loop that never sends the completion. You will need an extra condition to make the "collected" stream a finite one.
For infinite streams, the collect(_:) overload (the one that allows to pass a number of items to collect) performs better in regards to the demand, but still requests from upstream more elements than one might expect:
When this publisher receives a request for .max(n) elements, it requests .max(count * n) from the upstream publisher.

Swift Combine return result of first future after evaluation of second

I have the following situation:
2 futures, one returns a value I am interested in, the other does some operations and returns void. The 2 are not related to each other (so the code should not be mixed), but both need to be executed in the right order in the application logic.
What I want to do is subscribe to a publisher that does the following:
future one executes and gives a value
future two executes and returns nothing
the subscriber receives the value of future one after the execution of future two.
Here is a small code example that does not compile, that shows what I would like to achieve:
import Combine
func voidFuture() -> Future<Void, Error> {
return Future<Void, Error> { promise in
promise(.success(()))
}
}
func intFuture() -> Future<Int, Error> {
return Future<Int, Error> { promise in
promise(.success(1))
}
}
func combinedFuture() -> AnyPublisher<Int, Error> {
var intValue: Int!
return intFuture().flatMap { result in
intValue = result
return voidFuture()
}.flatMap{ _ in
return CurrentValueSubject(intValue).eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
combinedFuture()
.sink(receiveCompletion: { _ in }, receiveValue: { val in print(val)})
.store(in: &subscriptions)
You need to .map the Void result of the second publisher (voidFuture) back to the result of the first publisher (intFuture), which is what the .flatMap would emit:
func combinedFuture() -> AnyPublisher<Int, Error> {
intFuture().flatMap { result in
voidFuture().map { _ in result }
}
.eraseToAnyPublisher()
}

Swift Combine alternative to Rx Observable.create

I have some code that is built using RxSwift, and I'm playing around with converting it to use Apple's Combine framework.
One pattern which is very common is the use of Observable.create for one-shot observables (usually network requests). Something like this:
func loadWidgets() -> Observable<[Widget]> {
return Observable.create { observer in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
observer.onNext(widgets)
observer.onComplete()
}, error: { error in
// publish error on failure
observer.onError()
})
// allow cancellation
return Disposable {
loadTask.cancel()
}
}
}
I'm trying to map that across to Combine and I haven't been able to quite figure it out. The closest I've been able to get is using Future for something like this:
func loadWidgets() -> AnyPublisher<[Widget], Error> {
return Future<[Widget], Error> { resolve in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
resolve(.success(widgets))
}, error: { error in
// publish error on failure
resolve(.failure(error))
})
// allow cancellation ???
}
}
As you can see, it does most of it, but there's no ability to cancel.
Secondarily, future doesn't allow multiple results.
Is there any way to do something like the Rx Observable.create pattern which allows cancellation and optionally multiple results?
I think I found a way to mimic Observable.create using a PassthroughSubject in Combine. Here is the helper I made:
struct AnyObserver<Output, Failure: Error> {
let onNext: ((Output) -> Void)
let onError: ((Failure) -> Void)
let onComplete: (() -> 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)) },
onComplete: { subject.send(completion: .finished) }
))
}, receiveCancel: { disposable?.dispose() })
.eraseToAnyPublisher()
}
}
And here is how it looks in usage:
func loadWidgets() -> AnyPublisher<[Widget], Error> {
AnyPublisher.create { observer in
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
observer.onNext(widgets)
observer.onComplete()
}, error: { error in
observer.onError(error)
})
return Disposable {
loadTask.cancel()
}
}
}
From what I've learned, the support for initializing an AnyPublisher with a closure has been dropped in Xcode 11 beta 3. This would be a corresponding solution for Rx's Observable.create in this case, but for now I believe that the Future is a goto solution if you only need to propagate single value. In other cases I would go for returning a PassthroughSubject and propagating multiple values this way, but it will not allow you to start a task when the observation starts and I believe it's far from ideal compared to Observable.create.
In terms of cancellation, it does not have an isDisposed property similar to a Disposable, so it's not possible to directly check the state of it and stop your own tasks from executing. The only way that I can think of right now would be to observe for a cancel event, but it's surely not as comfortable as a Disposable.
Also, I'd assume that cancel might in fact stop tasks like network requests from URLSession based on the docs here: https://developer.apple.com/documentation/combine/cancellable
Add an isCancelled operation outside the closure and check it in the future's closure. isCancelled can be toggled with the handleEvent() operator.
var isCancelled = false
func loadWidgets() -> AnyPublisher<[Widget], Error> {
return HandleEvents<Future<Any, Error>> { resolve in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
resolve(.success(widgets))
}, error: { error in
// publish error on failure
resolve(.failure(error))
}
if isCancelled {
loadTask.cancel()
}
).handleEvents(receiveCancel: {
isCancelled = true
})
}
}
and somewhere in the app you do this to cancel the event
loadWidgets().cancel()
Also check this article
Thanks to ccwasden for the inspiration. This replicates Observable.create semantics with a pure Combine implementation without any superfluous entities.
AnyPublisher.create() Swift 5.6 Extension
public extension AnyPublisher {
static func create<Output, Failure>(_ subscribe: #escaping (AnySubscriber<Output, Failure>) -> AnyCancellable) -> AnyPublisher<Output, Failure> {
let passthroughSubject = PassthroughSubject<Output, Failure>()
var cancellable: AnyCancellable?
return passthroughSubject
.handleEvents(receiveSubscription: { subscription in
let subscriber = AnySubscriber<Output, Failure> { subscription in
} receiveValue: { input in
passthroughSubject.send(input)
return .unlimited
} receiveCompletion: { completion in
passthroughSubject.send(completion: completion)
}
cancellable = subscribe(subscriber)
}, receiveCompletion: { completion in
}, receiveCancel: {
cancellable?.cancel()
})
.eraseToAnyPublisher()
}
}
Usage
func doSomething() -> AnyPublisher<Int, Error> {
return AnyPublisher<Int, Error>.create { subscriber in
// Imperative implementation of doing something can call subscriber as follows
_ = subscriber.receive(1)
subscriber.receive(completion: .finished)
// subscriber.receive(completion: .failure(myError))
return AnyCancellable {
// Imperative cancellation implementation
}
}
}