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()
.