Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AnyCancellable.store(in:) with Combine

Tags:

swift

combine

Let’s say you are using the built-in .store(in:) method on AnyCancellable like so:

private var subscriptions = Set<AnyCancellable>()

let newPhotos = photos.selectedPhotos
newPhotos
  .map { [unowned self] newImage in
    return self.images.value + [newImage]
  }
  .assign(to: \.value, on: images)
  .store(in: &subscriptions)

If you have an app that does this a lot - are these removed when the publishers complete?

Also, If i decide to go with this approach instead:

private var newPhotosSubscription: AnyCancellable?

self.newPhotosSubscription = newPhotos
  .map { [unowned self] newImage in
    self.images.value + [newImage]
  }
  .assign(to: \.value, on: images)

Everytime I call the method again, it override the AnyCancellable, what happens to the previous one? Does it still complete before being deallocated?

like image 911
Jaythaking Avatar asked Sep 17 '20 13:09

Jaythaking


1 Answers

Dávid Pásztor's solution is close, but it has a race condition. Specifically, suppose the newPhotos publisher completes synchronously, before the sink method returns. Some publishers operate this way. Just and Result.Publisher both do, among others.

In that case, the completion block runs before sink returns. Then, sink returns an AnyCancellable which is stored in newPhotosSubscription. But the subscription has already completed, so newPhotosSubscription will never be set back to nil.

So for example if you use a URLSession.DataTaskPublisher in live code but substitute a Just publisher in some test cases, the tests could trigger the race condition.

Here's one way to fix this: keep track of whether the subscription has completed. Check it after sink returns, before setting newPhotosSubscription.

private var ticket: AnyCancellable? = nil

if ticket == nil {
    var didComplete = false
    let newTicket = newPhotos
        .sink(
            receiveValue: { [weak self] in
                self?.images.value.append($0)
            },
            receiveCompletion: { [weak self] _ in
                didComplete = true
                self?.ticket = nil
            }
        )
    if !didComplete {
        ticket = newTicket
    }
}

Everytime I call the method again, it override the AnyCancellable, what happens to the previous one? Does it still complete before being deallocated?

The previous one, if any, is cancelled, because the only reference to the old AnyCancellable is destroyed and so the AnyCancellable is destroyed. When an AnyCancellable is destroyed, it cancels itself (if not already cancelled).

like image 108
rob mayoff Avatar answered Sep 28 '22 01:09

rob mayoff