RxSwift BehaviorRelay cancel previous calls, only use the most recent - swift

I have a BehaviorRelay setup to store an array of addresses, and then I observe that BehaviorRelay so that I can create an MKAnnotation array and then display that on the map.
let addresses = BehaviorRelay<[Address]>(value: [])
I make network requests when the user moves the map to a new area.
If the user moves the map very quickly, I can end up with several network requests
I only want the latest response.
This is where my problem begins.
addresses.asObservable().subscribe(onNext: { [unowned self] (value) in
self.fetchAllAnnotationsAndAddToMap()
}).disposed(by: disposebag)
fetchAllAnnotationsAndAddToMapgets called every time addresses is set.
fetchAllAnnotationsAndAddToMap can take a long time to complete. Requests to run fetchAllAnnotationsAndAddToMapget stacked up and all of them run to completion.
What I want to happen is once addresses is set again, I want for all former calls to be discarded and only use the latest.
I've heard this is what flatMapLatest is for.
However, flatMapLatest requires I return an observable and I'm confused by that.
How do I cancel the methods called after the BehaviorRelay is updated and only use the most recent?

First you need to break up your fetchAllAnnotationsAndAddToMap() function into two functions. Then you can:
addresses.asObservable()
// you might want to put a `debounce` call in here.
.flatMapLatest { addresses in
return fetchAllAnnotations(from: addresses)
}
.subscribe(onNext: { [weak self] annotations in
self?.addToMap(annotations)
}
.disposed(by: disposeBag)

Probably using a flatMapLatest is a designated solution to this problem.However, I would like to propose another solution :
Simply re-initialize your BehaviorRelay again in your subscribe part of your code, and filter empty ones, Like this :
addresses.asObservable().filter({$0.count >0}).subscribe(onNext: { [unowned self] (value) in
self.fetchAllAnnotationsAndAddToMap()
self.addresses = BehaviorRelay<[Address]>(value: [])
}).disposed(by: disposebag)

Related

Swift Combine: handleEvents vs map

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

Swift Combine publishers vs completion handler and when to cancel

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)")
})
))
}

RxSwift: How to respond to a series of notifications?

Suppose I have two notifications coming one after another. I need to wait for work to complete from 1st notification and only then fire the work from 2nd notification. For now, I tried to schedule sequences to a serial scheduler, but it doesn't work as expected, it seems that I'm missing something.
NotificationCenter.default.rx.notification(.notification1)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.doSomeAsynchWork() //Fires another subscription, kind of ugly
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.notification2)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.doSomeWork() //This should only be executed after doSomeAsynchWork() is done
})
.disposed(by: disposeBag)
I was expecting work to be done in a serial manner, but that's not the case, my guess is that doSomeAsynchWork() is, well, asynchronous and doSomeWork() fires right after. But can I somehow wait for asynchronous work to complete? Any help is appreciated
UPD: notification2 may or may not arrive, so they are kind of independent of each other. Also, notification1 may or may not arrive, it's just different use cases in the app. But when both notification1 and notification2 are present, I need to wait for doSomeAsynchWork() to finish
The flow is as follows:
User taps to block some element in the list, which is only allowed for a signed-in user
User gets redirected to a sign-in screen
User sings in and then, notification1 fires
We continue to block that element now that we're signed in
Notification2 fires
The problem is when notification1 fired, we need to reload the screen, so that logic comes to doSomeAsynchWork(). On top of that, we're getting the "delete element" notification and we're trying to locate the element which is not there yet, so we're kind of stuck with an inconsistent state, where the element's blocked, but still present on a screen
The difficulty is that we can sign-in without blocking element and we can block element without the need of signing-in in (because we are already signed-in for example)
Based on the flow you described in your update, I would expect to see something like this:
func example(tapElement: Observable<ID>, isLoggedIn: Observable<Bool>, presentLogin: Observable<Void>) {
tapElement
.withLatestFrom(isLoggedIn) { (id: $0, isLoggedIn: $1) }
.flatMapFirst { id, isLoggedIn in
isLoggedIn ? Observable.just(id) : presentLogin.map { id }
}
.subscribe(onNext: { id in
blockElement(id: id)
})
}
I don't see any reason to have all the notifications in the first place.
Old Answer
I would have doSomeAsynchWork() return an Observable<Void> which emits an event with the async work is complete. Then I could:
NotificationCenter.default.rx.notification(.notification1)
.flatMap { doSomeAsynchWork() }
.subscribe(onNext: { doSomeWork() }
Another option would be to have doSomeAsynchWork() return a Completable, then you would do something like:
NotificationCenter.default.rx.notification(.notification1)
.flatMap { doSomeAsynchWork() }
.subscribe(onCompleted: { doSomeWork() }

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.

Realm notifications registration while in write transaction

I understand that you can not register a Realm .observe block on an object or collection if the Realm is in a write transaction.
This is easier to manage if everything is happening on the main thread however I run into this exception often because I prefer to hand my JSON parsing off to a background thread. This works great because I don't have to bog down the main thread and with Realm's beautiful notification system I can get notified of all modifications if I have already registered to listen for those changes.
Right now, if I am about to add an observation block I check to make sure my Realm is not in a write transaction like this:
guard let realm = try? Realm(), !realm.isInWriteTransaction else {
return
}
self.myToken = myRealmObject.observe({ [weak self] (change) in
//Do what ever
}
This successfully guards against this exception. However I never get a chance to re - register this token unless I get a little creative.
Does the Realm team have any code examples/ suggestions on a better pattern to avoid this exception? Any tricks I'm missing to successfully register the token?
In addition to the standard function, I do use an extension for Results to avoid this in general. This issue popped up, when our data load grew bigger and bigger.
While we do now rewrite our observe functions logic, this extension is an interims solution to avoid the crashes at a first place.
Idea is simple: when currently in a write transaction, try it again.
import Foundation
import RealmSwift
extension Results {
public func safeObserve(on queue: DispatchQueue? = nil,
_ block: #escaping (RealmSwift.RealmCollectionChange<RealmSwift.Results<Element>>) -> Void)
-> RealmSwift.NotificationToken {
// If in Write transaction, call it again
if self.realm?.isInWriteTransaction ?? false {
DispatchQueue.global().sync {
Thread.sleep(forTimeInterval: 0.1) // Better to have some delay than a crash, hm?
}
return safeObserve(on: queue, block)
}
// Aight, we can proceed to call Realms Observe function
else {
return self.observe(on: queue, block)
}
}
}
Then call it like
realmResult.safeObserve({ [weak self] (_: RealmCollectionChange<Results<AbaPOI>>) in
// Do anything
})