Sean Danzeiser
Sean Danzeiser

Reputation: 9243

Combine: How to cancel a flatMap'ed publisher

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

Answers (1)

rob mayoff
rob mayoff

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

Related Questions