3

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.

user2848557
  • 329
  • 3
  • 7
  • 1
    Interesting question! I don't know the answer, but I can tell you that code is a [mass noun](https://en.wikipedia.org/wiki/Mass_noun), so these are not known as "codes". Instead, they are "pieces of code". ‍♂️ – Jessy Apr 14 '20 at 21:36

1 Answers1

8

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.

rob mayoff
  • 342,380
  • 53
  • 730
  • 766
  • Interesting, so it's using an intentional strong retain cycle as a way of keeping both the subscription and sink alive until the cancel event arrives? Is there an external way to cause it to stop? – Alexander Apr 15 '20 at 13:45
  • 1
    `Subscribers.Sink` conforms to `Cancellable`. If you save the `Sink` in your own property, you can call its `cancel` method directly. – rob mayoff Apr 15 '20 at 13:49
  • Oh neat! Thanks for sharing – Alexander Apr 15 '20 at 13:53
  • Is there a way to extract the `Subscribers.Sink` object passed to an `AnyPublisher`? Because if I have to save the `Subscribers.Sink` to an external class property to call `cancel()` from elsewhere then it's the same thing as calling sink and saving the AnyCancelable it returns as a class property. – Parth Tamane May 20 '21 at 16:53
  • No, there is no way to get the subscribers out of an `AnyPublisher` or any other `Publisher` supplied in the SDK. If you want to cancel the subscription, you should just use `.sink`. – rob mayoff May 20 '21 at 17:15