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.
Related
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().
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
}
I have an observable inside a function.
The function happens in a certain queue, queueA, and the observable is subscribed to with observeOn(schedulerB). In onNext, I'm changing a class variable.
In another function, I'm changing the same class variable, from a different queue.
Here is some code to demonstrate my situation:
class SomeClass {
var commonResource: [String: String] = [:]
var queueA = DispatchQueue(label: "A")
var queueB = DispatchQueue(label: "B")
var schedulerB = ConcurrentDispatchQueueScheduler(queue: QueueB)
func writeToResourceInOnNext() {
let obs: PublishSubject<String> = OtherClass.GetObservable()
obs.observeOn(schedulerB)
.subscribe(onNext: { [weak self] res in
// this happens on queue B
self.commonResource["key"] = res
}
}
func writeToResource() {
// this happens on queue A
commonResource["key"] = "otherValue"
}
}
My question is, is it likely to have concurrency issues, if commonResource is modified in both places at the same time?
What is the common practice for writing/reading from class/global variables inside onNext in an observable with observeOn?
Thanks all!
Since your SomeClass has no control over when these functions will be called or on what threads the answer is yes, you are setup to have concurrency issues in this code due to its passive nature.
The obvious solution here is to dispatch to queue B inside writeToResource() in order to avoid the race condition.
Another option would be to use an NSLock (or NSRecursiveLock) and lock it before you write to the resource and unlock it after.
The best practice is: when you have a side effect happening inside a subscribe function's closure (in this case writing to commonResource that the closure is the only place where the side effect occurs. This would mean doing away with the passive writeToResource() function and instead passing in an Observable that was generated by whatever code currently is calling the function.
I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}
I've formulated a previous question on a subject related to this, but more specifically now: I'm curious about the behavior of a #State variable when inside a closure, apparently from what I've tested the variable can't be updated, I've looked around a few articles but couldn't find the answer. Specifically, this is what I've been dealing with:
private let disposeBag = DisposeBag()
#State var eventsView = [Event]()
func setObserver(){
EventGroup.shared.events.asObservable()
.subscribe(onNext: {
[unowned self] events in
print("Events Resp: "+String(events.count))
self.eventsView = events
print("Events Count: "+String(self.eventsView.count))
})
.disposed(by: disposeBag)
}
The result of the prints above is:
Events Resp: 3
Events Count: 0
How come, the eventsView variable is not updated?
From Apple's docs:
A State instance isn’t the value itself; it’s a means of reading and mutating the value. To access a state’s underlying value, use its value property.
Only access a state property from inside the view’s body (or from functions called by it). For this reason, you should declare your state properties as private, to prevent clients of your view from accessing it.