Reputation: 9243
New to Combine & reactive programming here so the help is very much appreciated.
I have the following scenario: I'd like to build a UI where a user can filter down content via various 'filter' buttons on the page. When a user taps one of the buttons, I need to shoot off an API request to get the data.
Now, I have a publisher that provides me with the 'state' of these selections, and I've structured my code like this:
state
.publisher /* sends whenever 'state' updates behind the scenes */
.debounce(for: 1.0, scheduler: DispatchQueue.main)
.map { /* create some URL request */ }
.flatMap {
URLSession.shared.dataTaskPublisher(for: someRequest)
.map { $0.data }
.decode(type: MyResponseType.self, decoder: JSONDecoder())
}.sink(receiveCompletion: { (completion) in
/// cancelled
}) { (output) in
/// go show my results
/// Ideally, this is only called when the most recent API call finishes!
}.store(in: &cancellables)
However, this implementation has a bug in the following scenario: If one event makes it through to the flatMap to fire off a request, and a subsequent event does the same before the network call completes, then we will call the completion handler twice.
Preferably, we are somehow a canceling the inner pipeline so we only execute the completion handler with the most recent event.
How can I 'cancel' that inner pipeline (the one started by the dataTaskPublisher) when new events come down the pipeline without tearing down the outer pipeline?
Upvotes: 2
Views: 2654
Reputation: 385600
You don't want flatMap
. You want switchToLatest
. Change your flatMap
to a plain map
, then add .switchToLatest()
after it. Because switchToLatest
requires the failure types to match up, you may also need to use mapError
. The decode
operator produces failure type Error
, so you can mapError
to Error
.
Example:
state
.publisher /* sends whenever 'state' updates behind the scenes */
.debounce(for: 1.0, scheduler: DispatchQueue.main)
.map { makeURLRequest(from: $0) }
.map({ someRequest in
URLSession.shared.dataTaskPublisher(for: someRequest)
.map { $0.data }
.decode(type: MyResponseType.self, decoder: JSONDecoder())
})
.mapError { $0 as Error }
.switchToLatest()
.sink(
receiveCompletion: ({ (completion) in
print(completion)
/// cancelled
}),
receiveValue: ({ (output) in
print(output)
/// go show my results
/// Ideally, this is only called when the most recent API call finishes!
}))
.store(in: &cancellables)
Upvotes: 10