How to stop storing AnyCancellable after Swift Combine Sink has received at least one value?

Viewed 4787

I have a sink that needs to be canceled as soon as I receive the first value. I don't care about future values, just the first one published. Because without storing the AnyCancelable created by the sink, the garbage collector will delete the sink, I must store it. At the same time, I must also clean it up after the sink has completed, otherwise, I will have a memory leak.

I built one using a UUID → AnyCancelable map, but I am worried that this is more complex than it should be; is there another way of doing this? What's recommended by Combine?

@Published var locationState: (location: CLLocation?, error: Error?)?
var requestLocationSinks: [String: AnyCancellable] = [:]

// 1. Generate ID to uniquely identify the current sink.
let sinkID = UUID().uuidString

// 2. Start the sink and store it in our ID → AnyCancellable dictionary.
requestLocationSinks[sinkID] = $locationState.sink { locationState in
    if let locationState = locationState {
        invokeCallbackWithLocationState(locationState)
    }
    // 3. Remove the stored AnyCancellable as soon as we received our first value!
    self.requestLocationSinks.removeValue(forKey: sinkID)
}
2 Answers

If you just need to keep it alive until the sink is called once, you can just create a temporary variable

var cancellable: AnyCancellable?
cancellable = $locationState.sink { locationState in
    if let locationState = locationState {
        invokeCallbackWithLocationState(locationState)
    }
    cancellable = nil
}

This will retain the AnyCancellable long enough (because the closure retains the reference)

if you are only interested in the first element of the upstream you can use either the operator .first() or the operator .prefix(1). They are equal. With .prefix(n) you can define the number of passed upstream elements.

myPublisher
     .first()
     .sink( ... )

or

myPublisher
     .prefix(1 or n)
     .sink( ... )

Please note, that the two operators above, cause an .finished completion. This means that the subscription will be deleted after the operation.

publisher is triggered -> receiveValue -> receiveCompletion (finished) -> subscription is deleted

If you work with AnyCancellable and want to delete your subscription, you should use the .cancel() function of the AnyCancellable .

var cancellable: AnyCancellable?
cancellable = myPublisher
     .sink(receiveValue: { value in 
          // Your logic.
          cancellable.cancel()
     })

I suppose that in your case the operator .first() is exactly what you need.

Related