RxSwift: How to respond to a series of notifications? - swift

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

Related

Is there a Combine-y way to stop multiple requests being triggered?

I currently have code like...
var isFetching = false
func fetch() {
guard !isFetching else { return }
fetching = true
apiPublisher
.receive(on: .main)
.handleEvents(receiveCompletion: { [weak self] _ in
self?.fetching = false
})
.assign(to: \.foo, on: self)
.store(in: &cancellables)
}
But I'm not happy with the way I'm stopping multiple requests happening like this. It works but feels clunky.
I feel like there should be a more "Combine-y" way of doing this.
Is there a more elegant/Combine-y way of doing this?
The issue here is the fact that you have the entire Publisher subscription chain in a function (fetch()) that can be called by virtually any other code at virtually any time from any thread. In order to do this in a "more Combine-y" you need to limit that. So the question is what calls this fetch() function? If you wrap all those call sites in a Publisher and then setup a trigger.flatMap { apiPublisher } and set up the flatMap to ignore events while it's waiting for the current network request to complete.
Something like this:
trigger
.flatMap(maxPublishers: .max(1)) {
apiPublisher
}
.receive(on: .main)
.assign(to: \.foo, on: self)
.store(in: &cancellables)
The important thing to note here is that you only execute this code once (probably in the viewDidLoad if this is a view controller. Then when you want to make the request, you send an event through the trigger Publisher. The flatMap will ensure that only one apiPublisher subscription will happen at a time.

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 BehaviorRelay cancel previous calls, only use the most recent

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)

RxSwift, Sometimes .disposed get called without calling .subscribe

In rx code, .disposed get called without any doing job like flatmap, subscribe. This happens only when I build my app at first time.
Does anybody knows what happens here?
This is my code
HTTP.getSomething()
.flatMap { (list) -> Single<Void> in
return HTTP.getList(withList: list)
}
.subscribe(onSuccess: { (storeList) in
log.debug("Finish!!!")
}, onError: { [weak self] (error) in
self?.presentAlert(error: error)
})
.disposed(by: self.disposeBag)
The only way the code you presented can possibly be disposed without attempting the work inside of the flatMap is if getSomething emits a completed without emitting a value, or if it emits an error, or if the disposeBag is deleted. One of those three things is happening.
Since you say it only happens on first build, I suspect that getSomething is trying to make a network call before it has all the info it needs which is causing it to emit an error.

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