Swift Combine erase array of publishers into AnyCancellable - swift

Is it possible to fire multiple requests which return a Publisher and be able to cancel them without sink?
I would like to combine the requests into a single cancellable reference or store each one if possible without sink (code below). Is this possible?
func fetchDetails(for contract: String) -> AnyPublisher<String, Error>
Fire Multiple requests and store
#State var cancellable: Set<AnyCancellable> = []
let promises = items.map {
self.fetchFeed.fetchDetails(for: $0.contract)
}
Publishers.MergeMany(promises)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in }) // ** is this required?
.store(in: &cancellable)

It really depends on what fetchDetails does to create the publisher. Almost every publisher provided by Apple has no side effects until you subscribe to it. For example, the following publishers have no side effects until you subscribe to them:
NSObject.KeyValueObservingPublisher (returned by NSObject.publisher(for:options:)
NotificationCenter.Publisher (returned by NotificationCenter.publisher(for:object:)
Timer.TimerPublisher (returned by Timer.publishe(every:tolerance:on:in:options:)
URLSession.DataTaskPublisher (returned by URLSession.dataTaskPublisher(for:)
The synchronous publishers like Just, Empty, Fail, and Sequence.Publisher.
In fact, the only publisher that has side effects on creation, as far as I know, is Future, which runs its closure immediately on creation. This is why you'll often see the Deferred { Future { ... } } construction: to avoid immediate side effects.
So, if the publisher returned by fetchDetails behaves like most publishers, you must subscribe to it to make any side effects happen (like actually sending a request over the network).

Related

Best way to call GlobalActor function from NotificationCenter Publisher

I have a GlobalActor with some methods on it that I'm using throughout my app. I'd like to be able to call a function from the actor when I receive a Notification from NotificationCenter, but can't call async functions from sink.
This is what I'm doing now:
class MyClass {
private var cancellables: [AnyCancellable] = []
init() {
NotificationCenter.default.publisher(for: NotificationName)
.receive(on: DispatchQueue.global(qos: .utility))
.compactMap { $0 as? SomeType }
.sink { [weak self] val in
Task { [weak self] in
await self?.someCallToActor(val)
}
}.store(in: &cancellables)
}
#SomeGlobalActor
func someCallToActor(_ val: String) async {
await SomeGlobalActor.shared.actorMethod(val)
}
}
...
#globalActor
actor SomeGlobalActor {
static var shared = SomeGlobalActor()
func actorMethod(_ val: String) async {
...
}
}
Calling Task within a closure here feels wrong and potentially race-condition-y. Is this the best way to accomplish what I'm trying to? I've tried receiving the notifications inside of the actor itself but it doesn't change much. The issue is the closure provided to sink is meant to be synchronous so I can't await inside of it.
The only way to get the Actor to do something is to put a message in its mail queue. The actor handles messages one at a time, in the order received. Every message that goes into the queue gets a response. Code can only put a message in the queue if it's willing, and able, to wait around for the response. The sink function can't wait around, it has other things to do (i.e. handle the next incoming messages from a Publisher). It needs an intermediary to do the waiting for it. The Task is that intermediary.
Note that the actor only prevents race conditions on the actor's state. As your intuition suggests, you could still have the "high-level" race condition of two messengers (two Tasks) racing to see who puts their item in Actor's mail queue first. But within the actor, there will be a strict ordering to the changes made by the two messages. (preventing low-level data races on the Actor's state)
Unfortunately the order of execution of independent Tasks, like the individual tasks created by your sink, is arbitrary. Your code could process notifications out-of-order.
To solve the problems you need to serialize the order in which the notifications are received and then delivered to the actor. To do that you need one Task, one messenger, doing both jobs – receiving the notifications and passing them on to the actor.
NotificationCenter allows you to receive the notifications as an AsyncSequence. So instead of getting messages as a publisher, you could get them from a sequence. Something like this:
class MyClass {
let notificationTask: Task<Void, Never>
init() {
notificationTask = Task {
for await notification in NotificationCenter.default.notifications(named: interestingNotification) {
guard !Task.isCancelled else { return }
if let value = notification.userInfo?[0] as? String {
await someActor.actorMethod(value)
}
}
}
}
}
Here the Task waits to receive a message from the notification center. When it gets one, it does some transformations on it (pulling values out of userInfo in this case) then it hands the transformed message over to the actor. The notifications arrival is serialized by the async sequence and the task makes sure that they arrive to the actor in the same order.

Swift Combine: handleEvents vs map

Let's assume you have a publisher that returns a list of some entity. Let's say it comes from a use case that fetches something from an api
protocol SomeAPI {
func fetchSomeEntity() -> AnyPublisher<[SomeEntity], Error>
}
Now you want to run some side effect on the output. Say, saving the result into a repository.
You would go with the handleEvents operator wouldn't you.
api.fetchSomeEntity().handleEvents(receiveOutput: {[unowned self] list in
repository.save(list)
})
But what if someone did that using/misusing the map operator:
api.fetchSomeEntity().map { [unowned self] list in
repository.save(list)
return list
}
Would you say there's something fundamentally wrong with that approach or is it just another path to the same end?
Neither of those operators are appropriate for your goals.
You should never do side effects in Combine pipelines, let alone executing map just for side effects, so calling repository.save inside a map is bad practice.
Side effects should only happen when handing back control to the imperative code from the functional Combine pipeline, so either in sink or in assign.
handleEvents on the other hand should only be used for debugging, not for production code as the docs clearly state.
Use handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:) when you want to examine elements as they progress through the stages of the publisher’s lifecycle.
The appropriate method you are looking for is sink. sink is the method to use when you want to execute side effects when a combine pipeline emits a value or completes. This is the method for handing back control to the iterative part of your code after the reactive pipeline.
api.fetchSomeEntity().sink(receiveCompletion: {
// handle errors here
}, receiveValue: { [unowned self] list in
repository.save(list)
}).store(in: &subscriptions)
If you want to do something like data caching in the middle of your pipeline, the way to do it is to break your pipeline. You can do this by doing the caching separately and updating an #Published property when the fetching succeeds, then observe that property from your view model and react to the property changing rather than the fetch succeeding.
class DataProvider {
#Published var entities: [SomeEntity] = []
func fetchAndCacheEntity() {
// you can replace this with `repository.save`, the main point is to update an `#Published` property
api.fetchSomeEntity().catch { _ in [] }.assign(to: &$entities)
}
}
Then in your viewModel, start the Combine pipeline on $entities rather than on api.fetchSomeEntity().

Combine`s subscribe(on:options:) operator

I have a question about the subscribe(on:options:) operator. I would appreciate if anyone can help me to figure it out.
So what we have from the documentation:
Specifies the scheduler on which to perform subscribe, cancel, and request operations.
In contrast with receive(on:options:), which affects downstream messages, subscribe(on:options:) changes the execution context of upstream messages.
Also, what I got from different articles is that unless we explicitly specify the Scheduler to receive our downstream messages on (using receive(on:options:)), messages will be send on the Scheduler used for receiving a subscription.
This information is not aligned with what I am actually getting during the execution.
I have the next code:
Just("Some text")
.map { _ in
print("Map: \(Thread.isMainThread)")
}
.subscribe(on: DispatchQueue.global())
.sink { _ in
print("Sink: \(Thread.isMainThread)")
}
.store(in: &subscriptions)
I would expect next output:
Map: false
Sink: false
But instead I am getting:
Map: true
Sink: false
The same thing happens when I use Sequence publisher.
If I swap the position of map operator and subscribe operator, I receive what I want:
Just("Some text")
.subscribe(on: DispatchQueue.global())
.map { _ in
print("Map: \(Thread.isMainThread)")
}
.sink { _ in
print("Sink: \(Thread.isMainThread)")
}
.store(in: &subscriptions)
Output:
Map: false
Sink: false
Interesting fact is that when I use the same order of operators from my first listing with my custom publisher, I receive the behaviour I want:
struct TestJust<Output>: Publisher {
typealias Failure = Never
private let value: Output
init(_ output: Output) {
self.value = output
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscriptions.empty)
_ = subscriber.receive(value)
subscriber.receive(completion: .finished)
}
}
TestJust("Some text")
.map { _ in
print("Map: \(Thread.isMainThread)")
}
.subscribe(on: DispatchQueue.global())
.sink { _ in
print("Sink: \(Thread.isMainThread)")
}
.store(in: &subscriptions)
Output:
Map: false
Sink: false
So I think there is either my total misunderstanding of all these mechanisms, or some publishers intentionally choose the thread to publish values (Just, Sequence -> Main, URLSession.DataTaskPublisher -> Some of Background), which does not make sense for me, cause in this case why would we need this subscribe(on:options:) for.
Could you please help me to understand what am I missing? Thank you in advance.
The first thing to understand is that messages flow both up a pipeline and down a pipeline. Messages that flow up a pipeline ("upstream") are:
The actual performance of the subscription (receive subscription)
Requests from a subscriber to the upstream publisher asking for a new value
Cancel messages (these percolate upwards from the final subscriber)
Messages that flow down a pipeline ("downstream") are:
Values
Completions, consisting of either a failure (error) or completion-in-good-order (reporting that the publisher emitted its last value)
Okay, well, as the documentation clearly states, subscribe(on:) is about the former: messages that flow upstream. But you are not actually tracking any of those messages in your tests, so none of your results reflect any information about them! Insert an appropriate handleEvents operator above the subscription point to see stuff flow upwards up the pipeline (e.g. implement its receiveRequest: parameter):
Just("Some text")
.handleEvents(receiveRequest: {
_ in print("Handle1: \(Thread.isMainThread)")
})
.map // etc.
Meanwhile, you should make no assumptions about the thread on which messages will flow downstream (i.e. values and completions). You say:
Also, what I got from different articles is that unless we explicitly specify the Scheduler to receive our downstream messages on (using receive(on:options:)), messages will be send on the Scheduler used for receiving a subscription.
But that seems like a bogus assumption. And nothing about your code determines the downstream-sending thread in a clear way. As you rightly say, you can take control of this with receive(on:), but if you don't, I would say you must assume nothing about the matter. Some publishers certainly do produce a value on a background thread, such as the data task publisher, which makes perfect sense (the same thing happens with a data task completion handler). Others don't.
What you can assume is that operators other than receive(on:) will not generally alter the value-passing thread. But whether and how an operator will use the subscription thread to determine the receive thread, that is something you should assume nothing about. To take control of the receive thread, take control of it! Call receive(on:) or assume nothing.
Just to give an example, if you change your opening to
Just("Some text")
.receive(on: DispatchQueue.main)
then both your map and your sink will report that they are receiving values on the main thread. Why? Because you took control of the receive thread. This works regardless of what you may say in any subscribe(on:) commands. They are different matters entirely.
Maybe if you call subscribe(on:) but you don't call receive(on:), some things about the downstream-sending thread are determined by the subscribe(on:) thread, but I sure wouldn't rely on there being any hard and fast rules about it; there's nothing saying that in the documentation! Instead, don't do that. If you implement subscribe(on:), implement receive(on:) too so that you are in charge of what happens.

In a Combine Publisher chain, how to keep inner objects alive until cancel or complete?

I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}

Swift Combine publishers vs completion handler and when to cancel

I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}