RxSwift Use withLatestFrom operator with multiple sources - ios

I have 3 observables namely source, source1 and source2. What I want is whenever source emits distinct event get the value of source1 and source2. This is the code I've come up with, obviously it won't compile since withLatestFrom expects only one observable.
source.distinctUntilChanged()
.withLatestFrom(source1, source2) { ($0, $1.0, $1.1) }
.subscribe(onNext: { (A, B, C) in
print("OnNext called")
})
.disposed(by: bag)

You almost have it. How about just combining source1 and source2?
source.distinctUntilChanged()
.withLatestFrom(Observable.combineLatest(source1, source2)) { ($0, $1.0, $1.1) }
.subscribe(onNext: { (A, B, C) in
print("OnNext called")
})
.disposed(by: bag)

You can use something like this:
valuesSource
.withUnretained(self) { ($0, $1.0, $1.1, $1.2, $1.3) }
.withLatestFrom(transactionSource) { ($0, $1) }
.withLatestFrom(userSource) { ($0.0, $0.1, $1) }
.map { values, transaction, user -> [SectionType] in
Hope this helps :)

You can do something like this
let source = PublishSubject<Int>()
let source1 = PublishSubject<Int>()
let source2 = PublishSubject<Int>()
Observable
.combineLatest([source,source1,source2])
.distinctUntilChanged { (oldArray, newArray) -> Bool in
return oldArray.first == newArray.first
}
.subscribe(onNext: { (values) in
debugPrint("source1 value is \(values[1]) and source2 value is \(values[2])")
})
.disposed(by: self.disposeBag)
source1.onNext(100)
source2.onNext(200)
source.onNext(3)
source.onNext(4)
source.onNext(4)
O/P will look like
Catch:
I am using combineLatest that means this will work only after all the observables emitted values at least once. You can always use startWith operator to ensure all your observables emits value at least once.

Related

RxSwift - block/stop/ignore event if condition met

After filtering an Observable list, I might have an empty list. I'm only interested in events that contain a populated list. Is there any way to stop empty events propagating to onNext?
let source: BehaviorRelay<[Int]> = .init(value: [])
source
.map { nums -> [Int] in
return nums.filter { $0 < 10 }
}
/// What can go here to stop/block/ignore an empty list
.subscribe(onNext: { nums in
print("OKPDX \(nums)")
})
source.accept([1, 9, 13])
// prints "[1, 9]" (all good)
source.accept([20, 22, 101])
// prints "[]" (not desirable, I'd rather not know)
What about using .filter? You could do this:
.map { nums -> [Int] in
return nums.filter { $0 < 10 }
}
.filter { !$0.isEmpty }
Or you could also validate that whenever you get an event like this:
.subscribe(onNext: { nums in
guard !nums.isEmpty else { return }
print("OKPDX \(nums)")
})

RxSwift Make Observable trigger a subject

I have a BehaviorSubject where my tableview is bound to through RxDataSources.
Besides that, I have a pull to refresh which creates an observable that updates the data and updates the data in the BehaviorSubject so that my UITableView updates correctly.
Now the question is, how do I handle the error handling for whenever my API call fails?
Few options that I have thought of was:
Subscribe to the observer's onError and call the onError of my BehaviorSubject\
Somehow try to concat? or bind(to: ..)
Let another subscriber in my ViewController subscribe besides that my tableview subscribes to the BehaviorSubject.
Any suggestions?
Ideally, you wouldn't use the BehaviorSubject at all. From the Intro to Rx book:
The usage of subjects should largely remain in the realms of samples and testing. Subjects are a great way to get started with Rx. They reduce the learning curve for new developers, however they pose several concerns...
Better would be to do something like this in your viewDidLoad (or a function that is called from your viewDidLoad):
let earthquakeData = Observable.merge(
tableView.refreshControl!.rx.controlEvent(.valueChanged).asObservable(),
rx.methodInvoked(#selector(UIViewController.viewDidAppear(_:))).map { _ in }
)
.map { earthquakeSummary /* generate URLRequest */ }
.flatMapLatest { request in
URLSession.shared.rx.data(request: request)
.materialize()
}
.share(replay: 1)
earthquakeData
.compactMap { $0.element }
.map { Earthquake.earthquakes(from: $0) }
.map { $0.map { EarthquakeCellDisplay(earthquake: $0) } }
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: EarthquakeTableViewCell.self)) { _, element, cell in
cell.placeLabel.text = element.place
cell.dateLabel.text = element.date
cell.magnitudeLabel.text = element.magnitude
cell.magnitudeImageView.image = element.imageName.isEmpty ? UIImage() : UIImage(named: element.imageName)
}
.disposed(by: disposeBag)
earthquakeData
.compactMap { $0.error }
.map { (title: "Error", message: $0.localizedDescription) }
.bind { [weak self] title, message in
self?.presentAlert(title: title, message: message, animated: true)
}
.disposed(by: disposeBag)
The materialize() operator turns a Event.error(Error) result into an Event.next(.error(Error)) so that the chain won't be broken down. The .compactMap { $0.element } emits only the successful results while the .compactMap { $0.error } emits only the errors.
The above code is adapted from my RxEarthquake sample.

Handle Connection Error in UITableView Binding (Moya, RxSwift, RxCocoa)

I'm using RxCoCoa and RxSwift for UITableView Biding.
the problem is when Connection lost or other connection errors except for Server Errors(I handled them) my app crash because of binding error that mentioned below. my question is how to handle Connection Errors?
fileprivate func getNextState() {
showFullPageState(State.LOADING)
viewModel.getProductListByID(orderGroup: OrderGroup.SERVICES.rawValue)
.do(onError: {
showStatusError(error: $0)
self.showFullPageState(State.CONTENT)
})
.filter {
$0.products != nil
}
.map {
$0.products!
}
.bind(to: (self.tableView?.rx.items(cellIdentifier: cellIdentifier, cellType: ProductCell.self))!) {
(row, element, cell) in
self.showFullPageState(State.CONTENT)
cell.product = element
}
.disposed(by: bag)
self.tableView?.rx.setDelegate(self).disposed(by: bag)
}
and this is my ViewModel :
func getProductListByID(orderGroup: String, page: String = "1", limit: String = "1000") -> Observable<ProductRes> {
return orderRegApiClient.getProductsById(query: getProductQueryDic(stateKey: getNextStateID(product: nextProduct)
, type: orderGroup, page: page, limit: limit)).map {
try JSONDecoder().decode(ProductRes.self, from: $0.data)
}.asObservable()
}
and I use Moya for my Network layer like This:
func getProductsById(query: [String: String]) -> Single<Response> {
return provider.rx.request(.getProductsById(query))
.filterSuccessfulStatusCodes()
}
You aren't handling errors anywhere. I mean you are acknowledging the error in the do operator but that doesn't actually handle it, that just allows it to pass through to the table view, which can't handle an error.
Look up the catchError series of operators for a solution. Probably .catchErrorJustReturn([]) will be all you need.
In a comment, you said:
... I don't want to return empty Array to my table. I want to show the error to customer and customer can retry service
In that case, you should use .catchError only for the success chain and setup a separate chain for the error as done below.
fileprivate func getNextState() {
showFullPageState(State.LOADING)
let products = viewModel.getProductListByID(orderGroup: OrderGroup.SERVICES.rawValue)
.share()
products
.catchError { _ in Observable.never() }
.filter { $0.products != nil }
.map { $0.products! }
.bind(to: tableView!.rx.items(cellIdentifier: cellIdentifier, cellType: ProductCell.self)) {
(row, element, cell) in
self.showFullPageState(State.CONTENT)
cell.product = element
}
.disposed(by: bag)
products
.subscribe(onError: { error in
showStatusError(error: error)
self.showFullPageState(State.CONTENT)
})
.disposed(by: bag)
self.tableView?.rx.setDelegate(self).disposed(by: bag)
}
The way you have the code setup, the only way for the user to retry the service is to call the function again. If you want to let the user retry in a more declarative manor, you would need to tie the chain to an observable that the user can trigger.

How to use combineLatest for two public subjects in RXSwfit

I'm new to RXSwift and I try to use combineLatest to combine the latest results from two public subjects
What I tried to do:
let sub1 = PublicSubject<Type1>()
let sub2 = PublicSubject<Type2>()
NetworkService1.fetch { sub1Value in
sub1.onNext(sub1Value)
}
NetworkService2.fetch { sub21Value in
sub2.onNext(sub2Value)
}
Observable.combineLatest(sub1.asObservable(), sub2.asObservable()) {
val1, val2 in
// do something with val1 and val2
// It seems it never hits this block
}
Not sure I'm doing the right thing.
It's better, if your NetworkService returns Observables. Then you don't have to create PublicSubjects and it's more beautiful.
I would do it like this:
let result1 = NetworkService1.shared.fetch()
let result2 = NetworkService2.shared.fetch()
Observable.combineLatest(result1, result2) { r1, r2 in
// Do stuff with r1, r2 with are APIResult<[YourModel]> and return result
}.subscribe(onNext: { result in
// You need to subscribe to run fetch methods
// Do stuff with result of combine latest
}).disposed(by: disposeBag)
This is example of a fetch method using Alamofire that returns Observable:
func fetch() -> Observable<APIResult<[YourModel]>> {
return Observable<APIResult<[YourModel]>>.create { (observer) -> Disposable in
Alamofire.request(yourURLString,
method: .post,
parameters: nil,
headers: APIManager.headers())
.responseJSON(completionHandler: { dataResponse in
switch dataResponse.result {
case .success(let value):
// parse value to someArray here
observer.onNext(APIResult.success(someArray))
case .failure(_):
guard let code = dataResponse.response?.statusCode else {
observer.onNext(APIResult.failure(APIError.unknownError))
break
}
observer.onNext(APIResult.failure(APIError.networkError(code:code)))
}
observer.onCompleted()
})
return Disposables.create()
}
}
APIResult allows you to pass errors too:
enum APIResult<Value> {
case success(Value)
case failure(APIError)
}
Just a couple of observations first:
There is no PublicSubject, it's PublishSubject (probably just a typo ;)
You don't need to call asObservable() in order to use PublishSubjects as arguments for combineLatest
You have to subscribe to your Observable (in this case Observable.combineLatest), otherwise nothing will happen.
Even if you subscribe correctly to Observable.combineLatest, you won't get the values, that have been emitted before the subscription, so those fetch calls have to be triggered after the subscription.
Since a piece of code is worth a thousand words:
let disposeBag = DisposeBag()
let sub1 = PublishSubject<String>()
let sub2 = PublishSubject<String>()
Observable.combineLatest(sub1, sub2) {
val1, val2 in
// do something with val1 and val2
// IT SHOULD WORK NOW
}.subscribe().disposed(by: disposeBag)
NetworkService1.fetch { sub1Value in
sub1.onNext(sub1Value)
}
NetworkService2.fetch { sub2Value in
sub2.onNext(sub2Value)
}

Best way to call multiple API requests in a for loop in RxSwift

I have to make several api calls (approx 100) using a for loop and on completion of this I need to complete the Observable. I am using it as following:
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
for (index,mat) in materialNo.enumerated(){
// Pass the material number one by one to get the Material object
self.getMaterialInfo(materialNo: mat).subscribe(onNext: { material in
var materialDict: [String: Material] = [:]
materialDict[material.materialNumber] = material
observable.onNext(materialDict)
if index == (materialNo.count-1){
observable.onCompleted()
}
}, onError: { (error) in
observable.onError(error)
}, onCompleted: {
}).disposed(by: self.disposeBag)
}
return Disposables.create()
})
}
Although loop is working fine and observable.onCompleted() is called but the caller method does not receive it.
I am calling it like following:
private func getImage(materialNo:[String]){
if materialNo.isEmpty {
return
}
var dictMaterials = [String:String]()
materialService.getMaterialInfo(materialNo: materialNo).subscribe(onNext: { (materials) in
for (key,value) in materials{
if (value.imageUrl != nil){
dictMaterials[key] = value.imageUrl
}
}
}, onError: { (error) in
}, onCompleted: {
self.view?.updateToolImage(toolImageList: dictMaterials)
}, onDisposed: {}).disposed(by: disposeBag)
}
OnCompleted block of Rx is not executing. How can I fix it?
Edit (5-March)
I revisited this answer, because I'm not sure what my brain was doing when I wrote the code sample below. I'd do something like this instead:
func getMaterialInfo(materialNo: String) -> Observable<[String: Material]> {
// ...
}
func getMaterialInfo(materialNumbers:[String]) -> Observable<[String: Material]>{
let allObservables = materialNumbers
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
Original answer
From your code, I interpret that all individual getMaterialInfo calls are done concurrently. Based on that, I would rewrite your getMaterialInfo(:[_]) method to use the .merge operator.
func getMaterialInfo(materialNo:[String]) -> Observable<[String: Material]>{
return Observable.create({ (observable) -> Disposable in
// a collection of observables that we haven't yet subscribed to
let allObservables = materialNo
.map { getMaterialInfo(materialNo: $0) }
return Observable.merge(allObservables)
}
return Disposables.create()
}
Note that using merge subscribes to all observable simultaneously, triggering 100 network requests at the same time. For sequential subscription, use concat instead!

Resources