Store subscription from custom Combine subscriber? - swift

According to the swift combine documentation, the proper way to connect a subscriber to a publisher is with the publishers subscribe<S>(S) protocol extension method. Then the publisher creates a subscription and passes that subscription on to the subscriber. All's well and good so far.
What I can't seem to figure out is how to gain access to that subscription and retain it in the calling code. How is sink() implemented such that it can return that subscription? Unless I'm mistaken, the subscription is responsible for retaining its subscriber, which means I can't store a reference to the subscription in the subscriber. And since Subscription isn't class bound, it can't be weak.

Here's an example of how sink might be implemented
import Combine
import Foundation
extension Publisher {
func mockSink(
receiveValue: #escaping (Output) -> Void,
receiveCompletion: #escaping (Subscribers.Completion<Failure>) -> Void) -> AnyCancellable {
var result : AnyCancellable!
let anySubscriber = AnySubscriber(
receiveSubscription: { subscription in
subscription.request(.unlimited)
result = AnyCancellable({subscription.cancel()})
},
receiveValue: { (value : Output) in receiveValue(value); return .unlimited},
receiveCompletion: receiveCompletion)
subscribe(anySubscriber)
return result
}
}
var subscriptions = Set<AnyCancellable>()
["one", "two", "three"].publisher
.mockSink(receiveValue: {debugPrint($0)}, receiveCompletion: {debugPrint($0)})
.store(in: &subscriptions)
As you can see the ability to return an AnyCancellable arises from the magic of AnySubscriber which returns its subscription in a closure. AnySubscriber is a handy tool to have when implementing custom Publishers and it may be helpful if you want to implement your own Subscriber type. But basically a Subscriber only exposes the subscription if it is designed to.

The cancellable retains the subscription (or possibly is the subscription. Remember Subscriptions are Cancellables.) So when you retain the Cancellable, you are retaining the subscription.

Related

How to have a publisher emit only to the last subscriber in Combine

Is there a way to have the publisher emit a value only to the latest subscriber/observer?
An example for that would be; a manager class that can be subscribed to by multiple observers. When an event occurs, I would like only the latest subscriber to be observed. As far as I know, there is no way for the publisher to keep track of its subscribers but my knowledge regarding Combine and reactive programming is limited so I am unsure if this is possible in the first place.
You are right. Unfortunately, there is no way to list/track subscribers of a publisher. To solve your problem, you have to implement a custom publisher. There are two possibilities here. Either you implement a custom publisher with the Publisher protocol, but Apple advises against this (see here), or you create a custom publisher with already existing types, as Apple recommends. I have prepared an example for the second option.
The logic is very simple. We create a publisher with a PassthroughSubject inside (it can also be a CurrentValueSubject). Then we implement the methods typical of a PassthroughSubject and use them to overwrite the same methods of the PassthroughSubject, which is inside our class. In the sink method we store all returning subscriptions BUT before we add a new subscription to the Set, we go through all the already cached subscriptions and cancel them. This way we achieve the goal that only the last subscription works.
// The subscriptions will be cached in the publisher.
// To avoid strong references, I use the WeakBox recommendation from the Swift forum.
struct WeakBox<T: AnyObject & Hashable>: Hashable {
weak var item: T?
func hash(into hasher: inout Hasher) {
hasher.combine(item)
}
}
class MyPublisher<T, E: Error> {
private let subject = PassthroughSubject<T, E>()
private var subscriptions = Set<WeakBox<AnyCancellable>>()
deinit {
subscriptions.removeAll()
}
public func send(_ input: T) {
subject.send(input)
}
public func send(completion: Subscribers.Completion<E>) {
subject.send(completion: completion)
}
public func sink(receiveCompletion receivedCompletion: #escaping (Subscribers.Completion<E>) -> Void, receiveValue receivedValue: #escaping (T) -> Void) -> AnyCancellable {
let subscription = subject
.sink(receiveCompletion: { completion in
receivedCompletion(completion)
}, receiveValue: { value in
receivedValue(value)
})
// Cancel previous subscriptions.
subscriptions.forEach { $0.item?.cancel() }
// Add new subscription.
subscriptions.insert(WeakBox(item: subscription))
return subscription
}
}
I tested the class in Playground as follows.
let publisher = MyPublisher<Int, Never>()
let firstSubscription = publisher
.sink(receiveCompletion: { completion in
print("1st subscription completion \(completion)")
}, receiveValue: { value in
print("1st subscription value \(value)")
})
let secondSubscription = publisher
.sink(receiveCompletion: { completion in
print("2st subscription completion \(completion)")
}, receiveValue: { value in
print("2st subscription value \(value)")
})
let thirdSubscription = publisher
.sink(receiveCompletion: { completion in
print("3st subscription completion \(completion)")
}, receiveValue: { value in
print("3st subscription value \(value)")
})
publisher.send(123)
Console output:
3st subscription value 123
If you comment out the line subscriptions.forEach { $0.cancel() }, then you get:
3st subscription value 123
1st subscription value 123
2st subscription value 123
Hopefully I could help you.

Swift Combine erase array of publishers into AnyCancellable

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

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

Subscribe returning Void instead of AnyCancelable

I currently have a publisher with the type of AnyPublisher<[MyClass], Error> to which I'm attempting to attach a subscriber and capture the resulting AnyCancelable. Xcode's autocomplete says I should be able to do this, but when the code is actually entered, I encounter a compiler error saying that the returned type isn't AnyCancelable, but ()
Here's an example of my code:
let networkController = NetworkController()
let viewState = MyViewState()
let publisher: AnyPublisher<[MyClass], Error> = networkController.createPublisher()
let cancelable: AnyCancellable = publisher.subscribe(viewState)
Cannot convert value of type '()' to specified type 'AnyCancellable'
My goal here is to wrap an existing async function, which could be called numerous times, in the Combine Framework, so that I can have a nice way of having the request get canceled when it's reassigned, like so:
... self.cancelable = cancelable
AnyPublisher has two subscribe methods for attaching a Subscriber to a Publisher. It inherits both from the Publisher protocol. Here they are:
func subscribe<S>(_ subscriber: S)
where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
func subscribe<S>(_ subject: S) -> AnyCancellable
where S : Subject, Self.Failure == S.Failure, Self.Output == S.Output
So, I guess your MyViewState type doesn’t conform to the Subject protocol. Therefore you can’t use the version of subscribe that returns AnyCancellable.
Subject is sub-protocol of Publisher that exposes send methods for injecting values which the Subject then publishes. Do you want your MyViewState class to be a Publisher? I suspect not.
What you probably want to do instead is change your MyViewState type to not even conform to Subscriber. Instead, use AnyPublisher’s sink method (also inherited from Publisher) to connect the publisher to your viewState. The sink method returns a Sink object that conforms to Cancellable:
func sink(
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
receiveValue: #escaping ((Output) -> Void))
-> Subscribers.Sink<Output, Failure>
You can wrap the returned Cancellable in AnyCancellable if needed. Thus:
let can = publisher.sink(
receiveCompletion: { viewModel.receive(completion: $0) },
receiveValue: { viewModel.receive($0) })
let anyCan = AnyCancellable(can)

Cleaning up observables

I am using rxSwift and I have a dictionary of observables which can be subscribed to.
var observables: [String: Observable<Bool>] = [:]
At some point I have to clean up some of those observables. I do it as follows
observables.removeValue(forKey: someKey)
Is it enough to clean up the observables with the above line? Is the observable also killed (or how do I "kill" it)? Someone might already be subscribed to the observable and then even after removing it from the dictionary it would still be alive and might fire, right?
Or is the observable gone the moment I remove it because nobody holds a strong reference to it? What happens in the moment the observable is removed to potential subsribers?
I do not have access to the subscribers from the class where the dictionary with the observables is kept.
You can use takeUntil operator. It will send a completed signal to the observable, so the subscriber will release the retained resources.
For example, you can setup a PublishSubject where you send the observable identifier to complete that observable.
var observables: [String: Observable<Bool>] = [:]
let finishObservable = PublishSubject<String>()
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let condition = finishObservable.filter { $0 == identifier }
let newObservable = observable.takeUntil(condition)
observables[identifier] = newObservable
return newObservable
}
This way, to clean an observable, you send the observable identifier and then you can remove the completed sequence from the dictionary.
func removeObservable(identifier: String) {
// Complete the observable so it stops sending events and subscriber releases resources
finishObservable.onNext(identifier)
observables.removeValue(forKey: identifier)
}
If you're planning to share subscription between observers, you can also use a ConnectableObservable. I've used this kind of observables when subscribers come and go but you want to share the same subscription. It's usefull if the observable fetches network resources for example.
var disposables: [String: Disposable] = [:]
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let newObservable: ConnectableObservable = observable.replay(1)
disposables[identifier] = newObservable.connect() // This call triggers the subscription, so you can call it later
return newObservable
}
func removeObservable(identifier: String) {
if let disposable = disposables.removeValue(forKey: identifier) {
disposable.dispose()
}
}