Swift Combine: handleEvents vs map - swift

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?

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

Swift Combine erase array of publishers into AnyCancellable

Is it possible to fire multiple requests which return a Publisher and be able to cancel them without sink?
I would like to combine the requests into a single cancellable reference or store each one if possible without sink (code below). Is this possible?
func fetchDetails(for contract: String) -> AnyPublisher<String, Error>
Fire Multiple requests and store
#State var cancellable: Set<AnyCancellable> = []
let promises = items.map {
self.fetchFeed.fetchDetails(for: $0.contract)
}
Publishers.MergeMany(promises)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in }) // ** is this required?
.store(in: &cancellable)
It really depends on what fetchDetails does to create the publisher. Almost every publisher provided by Apple has no side effects until you subscribe to it. For example, the following publishers have no side effects until you subscribe to them:
NSObject.KeyValueObservingPublisher (returned by NSObject.publisher(for:options:)
NotificationCenter.Publisher (returned by NotificationCenter.publisher(for:object:)
Timer.TimerPublisher (returned by Timer.publishe(every:tolerance:on:in:options:)
URLSession.DataTaskPublisher (returned by URLSession.dataTaskPublisher(for:)
The synchronous publishers like Just, Empty, Fail, and Sequence.Publisher.
In fact, the only publisher that has side effects on creation, as far as I know, is Future, which runs its closure immediately on creation. This is why you'll often see the Deferred { Future { ... } } construction: to avoid immediate side effects.
So, if the publisher returned by fetchDetails behaves like most publishers, you must subscribe to it to make any side effects happen (like actually sending a request over the network).

In a Combine Publisher chain, how to keep inner objects alive until cancel or complete?

I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}

What is the right approach for resolving save operations with Swift Combine

I have a classic situation where I want to commit some action only once. For example update some entity in database using Swift Combine. My problem is that I don't really know what is the best approach for doing something only once. How do I unsubscribe when the update is finished?
This is code snippet through the layers that I am currently using:
ViewModel:
let settingsModel: LocalSettingsModel
func saveLocalSettings(){
let cancelable = settingsUseCase
.saveLocalSettings(localSettingsModel: settingsModel)
.sink(receiveCompletion: {_ in
print("Completed!!!")
}) { _ in
print("Result of Save operation!!!")
}
}
UseCase:
func saveLocalSettings(settings: LocalSettingsModel) -> AnyPublisher<LocalSettingsModel, Error> {
return repository.saveLocalSettings(settings: settings)
}
Repository:
guard let realmSettings = LocalSettingsRealmModel(fromModel: settings) else {
return Fail<LocalSettingsModel, Error>(error: .postconditionError(errorMessage: ""))
.eraseToAnyPublisher()
}
return self.localDataSource
.saveLocalSettings(localSettings: realmSettings)
.receive(on: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
.mapError { (error) -> Error in
// do error mapping
}
.compactMap { settings in
return (LocalSettingsModel(fromModel: settings))
}
.eraseToAnyPublisher()
Data Source:
func saveLocalSettings(localSettings: LocalSettingsRealmModel) -> AnyPublisher<LocalSettingsRealmModel, LocalDataSourceError> {
do {
return Just(try saveSettings(localSettings: localSettings))
.mapError({ (Never) -> LocalDataSourceError in})
.eraseToAnyPublisher()
} catch let error as NSError {
// return some error
}
}
func saveSettings(localSettings: LocalSettingsRealmModel) throws -> LocalSettingsRealmModel
{
let realm = try Realm()
try realm.write {
realm.add(localSettings, update: .modified)
}
return localSettings
}
I would really appreciate some pointers in the direction of what is a good practice when we are not expecting continuous stream of information in reactive world like in case of functions whose purpose is to execute single action. Do I really need to use Just() like in this example or is there a way do deal with this kind of situations from subscriber side.
You want something that will convert a Publisher into a single value and then terminate, and the sequence operators in Combine are what you want to use for that kind of thing.
Combine is set up to deal with one OR many values. So you, as the consumer, need to give it some way to constrain the potentially many values into a single value, if you want to use an assign subscriber to set a value (or sink subscriber to invoke a closure where you do your save, in your case).
The sequence operators in Combine is where I'd think to look, but I can't really describe which one without knowing how many values and how you'd choose which one to apply. The two "easy" options are either first or last, but there's a variety of sequence operators that let you construct more complicated choices (including firstWhere and lastWhere which let you determine based on your own closure, which can be darned handy.
The embedded links are all to the online/free-version of Using Combine (disclosure: which I wrote) - and while I don't have any explicit examples about the sequence operators, I did flesh out the reference details for them in the book quite a bit.
Unless you're explicitly working from a publisher, you may find it easier to use a Promise library - depending on what's triggering your save. I don't know the Realm end of this to know what their API focuses on, and if you've made the publisher that's generating the data, or if that's coming from their API - and hence your desire to using Combine to solve this.

RxSwift: compactMap never executed

I'm trying to implement compactMap on RxSwift but it seems like is never executed.
Here is my code:
class MyClass{
var disposeBag = DisposeBag()
let subject = BehaviorRelay(value: 1)
func doSomething() {
Observable.from(optional: subject).compactMap{ $0
}.subscribe( onNext:{
print($0)
}).disposed(by: disposeBag)
subject.accept(2)
subject.accept(4)
subject.accept(5)
subject.accept(8)
}
}
When I change the value on subject the compactMap never gets called. Why not?
You are creating an Observable<BehaviorRelay<Int>> by using the from operator which only emits one value (the behavior relay itself) and then completes. The accept calls are being ignored because nothing is subscribing to the behavior relay itself.
I think you need to step back and figure out what you are trying to accomplish, and then read the documentation on the operators to find one that does what you need.

How to call every struct method inside write transaction

I created struct Repository for manipulating with objects of Realm database (changing some properties, adding new objects, deleting, etc.). When I want to write to the database, I have to do it inside do-try-catch block, so I created a method with completion which I call every time I need to write something to the database
private func action(_ completion: () -> Void) {
do {
try realm.write {
completion()
}
} catch {
print(error)
}
}
then I call methods for manipulating with objects like this:
func createObject(_ object: MyObject) {
action {
realm.add(object)
}
}
func deleteObject(_ object: MyObject) {
action {
realm.delete(object)
}
}
func setTitleForObject(_ object: MyObject, title: String) {
action {
object.title = title
}
}
...
My question is, is there any way how I can call every method inside this Repository struct inside write transaction in do-try-catch block by default instead of calling it inside completion of action? (or is some better way how to write to the Realm database without do-try-catch block?)
Short answer is no, there is no way to write data to realm without write transaction and without try-catch.
realm.write() is a convenient wrapper of transaction building with beginWrite() and commitWrite() calls.
These two functions build a transaction and commitWrite() is throwable, so you need to wrap to try-catch, anyway.
See https://realm.io/docs/swift/latest#writes
Example of using beginWrite()+commitWrite() https://realm.io/docs/swift/latest#interface-driven-writes
There are a lot of failures could happen during write transactions. So, simply, it is not safe to not to handle it somehow.
Also grouping write transactions by "action" is not a good idea if you going to process big amounts of objects because write transactions are costly. You'd rather group these changes to a single transaction instead of having a lot of small transactions.