RxSwift merge two api request into one result clears first result - ios

I have a refreshTrigger and BehaviourRelay of items:
var refreshTrigger = PublishSubject<Void>()
var rootItems: BehaviorRelay<[Codable]> = BehaviorRelay(value: [])
Then, I use UITextField to run search query when user enters text:
let queryText = queryTextField.rx.text.orEmpty
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
Finally, I have an observable which combines this observable with additional trigger to refresh manually:
let refreshText = refreshTrigger.withLatestFrom(queryText)
Observable.merge(refreshText, queryText)
.flatMapLatest { [weak self] query -> Observable<[Codable]> in
guard let strongSelf = self else { return .empty() }
let ctgs = try strongSelf.getCategories()
.startWith([])
.catchErrorJustReturn([])
let itms = try strongSelf.getSearchResults(query)
.retry(3)
.startWith([])
.catchErrorJustReturn([])
return Observable.merge(ctgs, itms)
}
.bind(to: rootItems)
.disposed(by: disposeBag)
As you can see, I want to send 2 requests: fetch categories and items, because I'm displaying them in the same UITableView. It sends both requests at the same time, but first result disappear when the second comes in. But I use merge, so it should work.
Why it doesn't show combined results?
Headers of the getCategories and getSearchResults looks like this:
func getSearchResults(_ text: String) throws -> Observable<[Codable]>
func getCategories() throws -> Observable<[Codable]>
they both use alamofire's rx extension to run queries.

Both of your getters return Observable arrays. This means that when the call completes, the observable emits an array of items. When you merge the two Observables, the follow on code can't distinguish between the items from one getter and the items from the other getter. It just sees an array come in (from one of them,) then another array come in (from the other.) In other words, you misunderstood how merge works.
To achieve the result you want, you should use zip or possibly combineLatest instead of merge. Something like this:
Observable.zip(ctgs, itms) { $0 + $1 }

Related

How do I get from Observable<BleHandler.BlePeripheral> to BleHandler.BlePeripheral?

I have a variable that gets me the type Observable<BleHandler.BlePeripheral> after using flatMap on the array.
let scannedPeripheral: Observable<BleHandler.BlePeripheral> = instance.bleScan()
.flatMap{ Observable.from($0)}
But now I need to use that variable in another function that takes BleHandler.BlePeripheral:
instance.bleEstablishConnection(scannedPeripheral: scannedPeripheral)
Obviously it doesn't work. Is there a way to get my Observable<BleHandler.BlePeripheral> to just BleHandler.BlePeripheral
It depends on whether or not the function returns a value and what type of value it returns...
If the function is void and you are just calling it for side effects then:
let disposable = scannedPeripheral
.subscribe(onNext: { instance.bleEstablishConnection(scannedPeripheral: $0) })
If your function has side effects and returns an Observable then:
let returnValue = scannedPeripheral
.flatMap { instance.bleEstablishConnection(scannedPeripheral: $0) }
If the function has no side effects and you are just calling it to transform your value into a different value then:
let returnValue = scannedPeripheral
.map { instance.bleEstablishConnection(scannedPeripheral: $0) }
This last one is unlikely based on the name of the function, but I put it here for completeness.

In RxCocoa/RxSwift, how to observe BehaviorRelay<[object]> array size changed

I'd like to subscribe to a BehaviorRelay<[object]>, and I'd like to execute some functions whenever we append or remove elements.
I've used the distinctUntilChange method
BehaviorRelay<[object]>.asObservable().distinctUntilChanged{ $0.count != $1.count}.subscribe{....}
But didn't work. What should I try? Should I try using other Subjects or Relays to achieve this purpose?
var objects = BehaviorRelay<[Object]>(value: [])
let disposeBag = DisposeBag()
objects.asObservable()
.subscribe(onNext: { (objects) in
//Do something only when appending or removing elements.
}).disposed(by: disposeBag)
//For example
let tempObj = objects.value
tempObj.append(newObj)
objects.accept(tempObj)//this will be called
tempObj.removeAll()
objects.accept(tempObj)//this will be called
tempObj.property = "Change Property"
objects.accept(tempObj)//this will NOT be called
From documentation:
parameter comparer: Equality comparer for computed key values.
I believe you should check the Equality with == operator. So, in your case, try this way:
BehaviorRelay<[object]>
.asObservable()
.distinctUntilChanged{ $0.count == $1.count}
.subscribe{....}

RxSwift using MVP subscribe from the Presenter

I have a searchBar to query results from an API. If i query directly from the VC without passing the text to the presenter i get this which works perfectly fine:
searchBar.rx.text
.skip(1)
.throttle(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest({ (text) -> Observable<[MarvelCharacter]> in
return GetCharacters().execute(offset: 0, name: text)
}).subscribe(onNext: { (characters) in
self.showCharacters(characters: characters)
}).addDisposableTo(disposeBag)
What i really want is to subscribe from GetCharacters() in the presenter.
Could be like this:
ViewController
searchBar.rx.text
.skip(1)
.throttle(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.map(presenter.searchTextDidChanged)
.subscribe().addDisposableTo(disposeBag)
Presenter
func searchTextDidChanged(text: String) {
getCharacters.execute(offset: offset, name: text)
.subscribe(onNext: { characters in
(self.offset == 0) ?
self.ui.showCharacters(characters: characters) : self.ui.appendCharacters(characters: characters)
}).addDisposableTo(disposeBag)
}
But sometimes i send RxSw (show the result RxSwift) and RxSwZ (empty results), but since i lost the “chain”, getCharacters from the presenter gets executed twice. Then if the 2nd request terminates before the good request i end up in the searchBar with RxSw5 and in the results with RxSwift, how can i deal with this?

How to convert Observable array of a type to Observable array of a different type with RxSwift

I'm new to RxSwift and I came across the next situation:
self.viewModel!.country
.flatMapLatest { country -> Observable<City> in
country!.cities!.toObservable()
}
.map { city -> SectionModel<String, String> in
SectionModel(model: city.name!, items: city.locations!)
}
.toArray()
.bindTo(self.tableView.rx_itemsWithDataSource(dataSource))
.addDisposableTo(self.disposeBag)
Seems to me like toArray() does nothing and I don't know why. On the other hand this code does what I want. I would like to know why the previous code does not work the same way:
self.viewModel!.country
.flatMapLatest { country -> Observable<[SectionModel<String, String>]> in
var models = [SectionModel<String, String>]()
for city in country!.cities! {
models.append(SectionModel(model: city.name!, items: city.locations!))
}
return Observable.just(models)
}
.bindTo(self.tableView.rx_itemsWithDataSource(dataSource))
.addDisposableTo(self.disposeBag)
Thanks in advance.
EDIT:
View model implementation is as follow:
class LocationViewModel {
let country = BehaviorSubject<Country?>(value: nil)
}
Country has a property 'cities' which is an array of City.
#solidcell You must be right, if I put debug() before and after toArray() I get 2 subscriptions, one for each debug(), and 1 next event for each array item only for the before toArray() debug().
But then, why isn't it completing?
It's probably because country!.cities for some countries doesn't terminate. toArray() will not emit any elements until the Observable completes. Only once the Observable completes does it package all of the elements into one single element as an Array. However, I can't say for certain if this is your issue, since I don't know how you've implemented country!.cities.
Try putting a .debug() in there and see if you can spot if it's because an Observable of cities isn't completing.

RXSwift - takeUntil canceling before next event

Following a similar example to question 39 here: http://reactivex.io/learnrx/
I'm trying to transform a method call search(query: String) into a sequence of those calls.
They way I'm achieving this is by creating a Variable which I update with the query value every time
the search(query: String) method is called.
Then I have this in my init():
_ = queryVariable.asObservable().flatMap({ query -> Observable<[JSON]> in
return self.facebookSearch(query).takeUntil(self.queryVariable.asObservable())
}).subscribeNext({ result in
if let name = result[0]["name"].string {
print(name)
} else {
print("problem")
}
})
If I type "ABC", my search(query: String) method will be called 3 times with "A", "AB", "ABC".
That would be mapped to seq(["A", "AB", "ABC"]) with queryVariable.asObservable().
Then I'm mapping it to Facebook searches (searching people by their names on Facebook).
And with subscribeNext I print the name.
If I don't use the takeUntil, it works as I'd expect, I get 3 sets of results, one for each of my queries("A", "AB", "ABC").
But if I type fast (before Facebook has time to respond to the request), I'd want only one result, for the query "ABC". That's why I added the takeUntil. With it I'd expect the facebookSearch(query: String) call to be ignored when the next query comes in, but it is being canceled for the current query, so with this takeUntil I end up printing nothing.
Is this a known issue or am I doing something wrong?
I used your code and found two solutions to your problem:
1. Use flatMapLatest
You can just use flatMapLatest instead of flatMap and takeUntil. flatMapLatest only returns the results of the latest search request and cancels all older requests that have not returned yet:
_ = queryVariable.asObservable()
.flatMapLatest { query -> Observable<String> in
return self.facebookSearch(query)
}
.subscribeNext {
print($0)
}
2. Use share
To make your approach work you have to share the events of your queryVariable Observable when you also use it for takeUntil:
let queryObservable = queryVariable.asObservable().share()
_ = queryObservable
.flatMap { query -> Observable<String> in
return self.facebookSearch(query).takeUntil(queryObservable)
}
.subscribeNext {
print($0)
}
If you do not share the events, the searchQuery.asObservable() in takeUntil creates its own (duplicate) sequence. Then when a new value is set on the searchQuery Variable it immediately fires a Next event in the takeUntil() sequence and that cancels the facebookSearch results.
When you use share() the sequence in takeUntil is observing the same event as the other sequence and in that case the takeUntil sequence handles the Next event after the facebookSearch has returned a response.
IMHO the first way (flatMapLatest) is the preferred way on how to handle this scenario.

Resources