Home > Blockchain >  Swift Combine: handleEvents vs map
Swift Combine: handleEvents vs map

Time:08-05

Let's assume you have a publisher that returns a list of some entity. Let's say it comes from a use case that fetches something from an api

protocol SomeAPI {
    func fetchSomeEntity() -> AnyPublisher<[SomeEntity], Error>
}

Now you want to run some side effect on the output. Say, saving the result into a repository.

You would go with the handleEvents operator wouldn't you.

    api.fetchSomeEntity().handleEvents(receiveOutput: {[unowned self] list in 
         repository.save(list) 
    })

But what if someone did that using/misusing the map operator:

api.fetchSomeEntity().map { [unowned self] list in
   repository.save(list)
   return list
}

Would you say there's something fundamentally wrong with that approach or is it just another path to the same end?

CodePudding user response:

Neither of those operators are appropriate for your goals.

You should never do side effects in Combine pipelines, let alone executing map just for side effects, so calling repository.save inside a map is bad practice.

Side effects should only happen when handing back control to the imperative code from the functional Combine pipeline, so either in sink or in assign.

handleEvents on the other hand should only be used for debugging, not for production code as the docs clearly state.

Use handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:) when you want to examine elements as they progress through the stages of the publisher’s lifecycle.

The appropriate method you are looking for is sink. sink is the method to use when you want to execute side effects when a combine pipeline emits a value or completes. This is the method for handing back control to the iterative part of your code after the reactive pipeline.

api.fetchSomeEntity().sink(receiveCompletion: { 
    // handle errors here
}, receiveValue: { [unowned self] list in 
    repository.save(list) 
}).store(in: &subscriptions)

If you want to do something like data caching in the middle of your pipeline, the way to do it is to break your pipeline. You can do this by doing the caching separately and updating an @Published property when the fetching succeeds, then observe that property from your view model and react to the property changing rather than the fetch succeeding.

class DataProvider {

    @Published var entities: [SomeEntity] = []

    func fetchAndCacheEntity() {
        // you can replace this with `repository.save`, the main point is to update an `@Published` property
        api.fetchSomeEntity().catch { _ in [] }.assign(to: &$entities)
    }
}

Then in your viewModel, start the Combine pipeline on $entities rather than on api.fetchSomeEntity().

  • Related