What's the difference between .sink and Subscribers.Sink? - swift

I want to do an asynchronous job with Future.
But the below .sink() closures never get called.
It seems that the instance of Future was released right after it was called.
Future<Int, Never> { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success(1))
}
}
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: {
print($0)
})
So I replaced .sink() closures to .subscribe(Subscribers.Sink()) like below. It works fine.
But the problem is I don't understand why it works fine. :(
It looks the same to me.
What is the difference between these two codes? And when can I use .sink(), and when can I not?
Future<Int, Never> { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success(1))
}
}
.receive(on: DispatchQueue.main)
.subscribe(Subscribers.Sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: {
print($0)
}))
Thanks in advance.

The .sink operator does three things:
It creates a Subscribers.Sink using the two closures you pass it.
It calls subscribe on the upstream Publisher, passing the Sink it created.
It creates an AnyCancellable that, when destroyed, cancels the Sink. It returns a reference to this AnyCancellable.
AnyCancellable is a reference-counted object. When the last reference to the AnyCancellable is destroyed, the AnyCancellable itself is destroyed. At that time, it calls its own cancel method.
In your first example, you are not saving the AnyCancellable returned by .sink. So Swift destroys it immediately, which means it cancels the subscription immediately. One second later, your asyncAfter closure calls promise, but the subscription has already been cancelled, so your receiveValue closure is not called.
In your second example, since you are creating the Subscribers.Sink object and passing it to subscribe yourself, no AnyCancellable is created to wrap the Sink. So nothing automatically destroys the subscription. One second later, the asyncAfter closure calls promise. Since the subscription wasn't destroyed, it still exists, so your receiveValue closure is called, and then your receiveCompletion closure is called.
So this is actually a very interesting use of Subscribers.Sink instead of the .sink operator. With .sink, you must save the returned AnyCancellable, else the subscription is cancelled immediately. But by using Subscribers.Sink directly, you create a subscription that lasts until it is completed, and you don't have to save anything. And when the subscription completes (with either .finished or .failure), the Sink discards the Subscription, which breaks the retain cycle that was keeping it alive, so the Sink and the Subscription are also destroyed, leaving no memory leaks.

Related

Can Combine be used in struct (instead of class)?

When using Combine as below
var cancellables: [AnyCancellable] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
URLSession.shared.dataTaskPublisher(for: tuple.imageURL)
.sink(
receiveCompletion: {
completion in
switch completion {
case .finished:
break
case .failure( _):
return
}},
receiveValue: { data, _ in DispatchQueue.main.async { [weak self] in self?.displayFlag(data: data, title: tuple.name) } })
.store(in: &cancellables)
}
We don't need to call cancel in the deinit as below
deinit {
cancellables.forEach {
$0.cancel()
}
}
Given that in https://developer.apple.com/documentation/combine/anycancellable, it is stated:
An AnyCancellable instance automatically calls cancel() when deinitialized.
Given we don't need to release during deinit, can the Combine be used in struct instead of class?
To answer your question directly, AnyCancellable does not rely on being stored in a class in order to cancel itself. Like any ref-counted object, it can be stored in a struct just fine, and it will be properly de-initialized and thus cancelled when there are no more references to it.
That said, you are correct to be suspicious here. You probably don't want to store an AnyCancellable in a struct the way you are doing it here. For starters, you would have to mark your loadItems function as mutating to even get it to compile, because storing the AnyCancellable means mutating the cancellables array.
Typically, if you're storing an AnyCancellable then you are associating that operation with something that has true identity, and thus is better represented as a class. You are basically saying "cancel this operation when this instance goes away". For example, if you're downloading an image to display in a UIViewController, you probably want to cancel that download if the UIViewController goes away because the user dismissed it; that is to say, the download operation is associated with a particular instance of UIViewController.
Since structs have value semantics, it is almost conceptually incoherent to have an AnyCancellable associated with an "instance" of a struct. Structs don't have instances, they just have values. When you pass a struct as an argument to a function, it creates a copy. That means if the function called loadItems then only the function's own copy of the struct value would store the AnyCancellable, and the operation would be immediately cancelled when the function returns because your original copy of the value is not storing the AnyCancellable.
You don't need deinit and don't need to call
cancellables.forEach {
$0.cancel()
}
I agree it's quite confusing that AnyCancellable have method cancel, that actually you don't need to call.
Publishers are automatically cancelled, when cancellables got disposed.
That is why you receive nothing if forget to store them somewhere.

With Combine, how to deallocate the Subscription after a network request

If you use Combine for network requests with URLSession, then you need to save the Subscription (aka, the AnyCancellable) - otherwise it gets immediately deallocated, which cancels the network request. Later, when the network response has been processed, you want to deallocate the subscription, because keeping it around would be a waste of memory.
Below is some code that does this. It's kind of awkward, and it may not even be correct. I can imagine a race condition where network request could start and complete on another thread before sub is set to the non-nil value.
Is there a nicer way to do this?
class SomeThing {
var subs = Set<AnyCancellable>()
func sendNetworkRequest() {
var request: URLRequest = ...
var sub: AnyCancellable? = nil
sub = URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: MyResponse.self, decoder: JSONDecoder())
.sink(
receiveCompletion: { completion in
self.subs.remove(sub!)
},
receiveValue: { response in ... }
}
subs.insert(sub!)
I call this situation a one-shot subscriber. The idea is that, because a data task publisher publishes only once, you know for a fact that it is safe to destroy the pipeline after you receive your single value and/or completion (error).
Here's a technique I like to use. First, here's the head of the pipeline:
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
let pub : AnyPublisher<UIImage?,Never> =
URLSession.shared.dataTaskPublisher(for: url)
.map {$0.data}
.replaceError(with: Data())
.compactMap { UIImage(data:$0) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
Now comes the interesting part. Watch closely:
var cancellable: AnyCancellable? // 1
cancellable = pub.sink(receiveCompletion: {_ in // 2
cancellable?.cancel() // 3
}) { image in
self.imageView.image = image
}
Do you see what I did there? Perhaps not, so I'll explain it:
First, I declare a local AnyCancellable variable; for reasons having to do with the rules of Swift syntax, this needs to be an Optional.
Then, I create my subscriber and set my AnyCancellable variable to that subscriber. Again, for reasons having to do with the rules of Swift syntax, my subscriber needs to be a Sink.
Finally, in the subscriber itself, I cancel the AnyCancellable when I receive the completion.
The cancellation in the third step actually does two things quite apart from calling cancel() — things having to do with memory management:
By referring to cancellable inside the asynchronous completion function of the Sink, I keep cancellable and the whole pipeline alive long enough for a value to arrive from the subscriber.
By cancelling cancellable, I permit the pipeline to go out of existence and prevent a retain cycle that would cause the surrounding view controller to leak.
Below is some code that does this. It's kind of awkward, and it may not even be correct. I can imagine a race condition where network request could start and complete on another thread before sub is set to the non-nil value.
Danger! Swift.Set is not thread safe. If you want to access a Set from two different threads, it is up to you to serialize the accesses so they don't overlap.
What is possible in general (although not perhaps with URLSession.DataTaskPublisher) is that a publisher emits its signals synchronously, before the sink operator even returns. This is how Just, Result.Publisher, Publishers.Sequence, and others behave. So those produce the problem you're describing, without involving thread safety.
Now, how to solve the problem? If you don't think you want to actually be able to cancel the subscription, then you can avoid creating an AnyCancellable at all by using Subscribers.Sink instead of the sink operator:
URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: MyResponse.self, decoder: JSONDecoder())
.subscribe(Subscribers.Sink(
receiveCompletion: { completion in ... },
receiveValue: { response in ... }
))
Combine will clean up the subscription and the subscriber after the subscription completes (with either .finished or .failure).
But what if you do want to be able to cancel the subscription? Maybe sometimes your SomeThing gets destroyed before the subscription is complete, and you don't need the subscription to complete in that case. Then you do want to create an AnyCancellable and store it in an instance property, so that it gets cancelled when SomeThing is destroyed.
In that case, set a flag indicating that the sink won the race, and check the flag before storing the AnyCancellable.
var sub: AnyCancellable? = nil
var isComplete = false
sub = URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: MyResponse.self, decoder: JSONDecoder())
// This ensures thread safety, if the subscription is also created
// on DispatchQueue.main.
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
isComplete = true
if let theSub = sub {
self?.subs.remove(theSub)
}
},
receiveValue: { response in ... }
}
if !isComplete {
subs.insert(sub!)
}
combine publishers have an instance method called prefix which does this:
func prefix(_ maxLength: Int) -> Publishers.Output<Self>
https://developer.apple.com/documentation/combine/publisher/prefix(_:)
playground example

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

RxSwift: How to respond to a series of notifications?

Suppose I have two notifications coming one after another. I need to wait for work to complete from 1st notification and only then fire the work from 2nd notification. For now, I tried to schedule sequences to a serial scheduler, but it doesn't work as expected, it seems that I'm missing something.
NotificationCenter.default.rx.notification(.notification1)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.doSomeAsynchWork() //Fires another subscription, kind of ugly
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.notification2)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.doSomeWork() //This should only be executed after doSomeAsynchWork() is done
})
.disposed(by: disposeBag)
I was expecting work to be done in a serial manner, but that's not the case, my guess is that doSomeAsynchWork() is, well, asynchronous and doSomeWork() fires right after. But can I somehow wait for asynchronous work to complete? Any help is appreciated
UPD: notification2 may or may not arrive, so they are kind of independent of each other. Also, notification1 may or may not arrive, it's just different use cases in the app. But when both notification1 and notification2 are present, I need to wait for doSomeAsynchWork() to finish
The flow is as follows:
User taps to block some element in the list, which is only allowed for a signed-in user
User gets redirected to a sign-in screen
User sings in and then, notification1 fires
We continue to block that element now that we're signed in
Notification2 fires
The problem is when notification1 fired, we need to reload the screen, so that logic comes to doSomeAsynchWork(). On top of that, we're getting the "delete element" notification and we're trying to locate the element which is not there yet, so we're kind of stuck with an inconsistent state, where the element's blocked, but still present on a screen
The difficulty is that we can sign-in without blocking element and we can block element without the need of signing-in in (because we are already signed-in for example)
Based on the flow you described in your update, I would expect to see something like this:
func example(tapElement: Observable<ID>, isLoggedIn: Observable<Bool>, presentLogin: Observable<Void>) {
tapElement
.withLatestFrom(isLoggedIn) { (id: $0, isLoggedIn: $1) }
.flatMapFirst { id, isLoggedIn in
isLoggedIn ? Observable.just(id) : presentLogin.map { id }
}
.subscribe(onNext: { id in
blockElement(id: id)
})
}
I don't see any reason to have all the notifications in the first place.
Old Answer
I would have doSomeAsynchWork() return an Observable<Void> which emits an event with the async work is complete. Then I could:
NotificationCenter.default.rx.notification(.notification1)
.flatMap { doSomeAsynchWork() }
.subscribe(onNext: { doSomeWork() }
Another option would be to have doSomeAsynchWork() return a Completable, then you would do something like:
NotificationCenter.default.rx.notification(.notification1)
.flatMap { doSomeAsynchWork() }
.subscribe(onCompleted: { doSomeWork() }

RxSwift, Sometimes .disposed get called without calling .subscribe

In rx code, .disposed get called without any doing job like flatmap, subscribe. This happens only when I build my app at first time.
Does anybody knows what happens here?
This is my code
HTTP.getSomething()
.flatMap { (list) -> Single<Void> in
return HTTP.getList(withList: list)
}
.subscribe(onSuccess: { (storeList) in
log.debug("Finish!!!")
}, onError: { [weak self] (error) in
self?.presentAlert(error: error)
})
.disposed(by: self.disposeBag)
The only way the code you presented can possibly be disposed without attempting the work inside of the flatMap is if getSomething emits a completed without emitting a value, or if it emits an error, or if the disposeBag is deleted. One of those three things is happening.
Since you say it only happens on first build, I suspect that getSomething is trying to make a network call before it has all the info it needs which is causing it to emit an error.