How can I update tableView from BehaviorRelay observable? - ios

I am trying to do searching in the table view, with the throttle.
Here I have BehaviorRelay in ViewModel
var countryCodes: BehaviorRelay<[CountryDialElement]> = BehaviorRelay<[CountryDialElement]>(value:[])
Here I have BehaviorRelay for the entered text
var searchText: BehaviorRelay<String> = BehaviorRelay<String>(value: "")
Here I have Table View which is binded with Observable from View Model
self.viewModel.params.countryCodes.bind(to: tableView.rx.items(cellIdentifier: "CountryDialTableViewCell")) { index, model, cell in
let countryCell = cell as! CountryDialTableViewCell
countryCell.configure(model)
}.disposed(by: disposeBag)
Here I have Rx binding to UISearchBar in ViewController
searchBar
.rx
.text
.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [weak self] query in
guard
let query = query.element else { return }
self?.viewModel.params.searchText.accept(query)
}
.disposed(by: disposeBag)
Then in ViewModel, I am trying to filter data and push filtered data to dataSource, to update the tableView.
Observable.combineLatest(params.countryCodes, params.searchText) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased()) || item.dialCode.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
self.params.countryCodes.accept(resultArray)
})
.disposed(by: disposeBag)
But I am getting this type of Error
Reentrancy anomaly was detected.
This behavior breaks the grammar because there is overlapping between sequence events.
The observable sequence is trying to send an event before sending the previous event has finished.
Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code, or that the system is not behaving in an expected way.
I am trying to achieve:
Binded table view with an observable array
Type text in the search bar
Filter observable, array
Update observable, reload tableView.

First thing I noticed... You have two BehaviorRelays defined as var. You should always define them with let.
You haven't posted enough code to demonstrate the error but the fundamental problem, as explained to you in the error message, is that you are breaking the observable grammar because you are pushing data through an Observable while in the middle of pushing data. If allowed, it would form an infinite recursive call that would overflow the stack.
Don't send an event until after the current event is finished sending. It would help a lot if you didn't use so many Relays...
You don't say anything about where the items in the array come from which also makes it hard to help...
Consider something like this:
Observable.combineLatest(
searchBar.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.startWith(""),
sourceOfItems(),
resultSelector: { searchTerm, items in
items.filter { $0.code.contains(searchTerm) }
}
)
.bind(to: tableView.rx.items(cellIdentifier: "CountryDialTableViewCell")) { index, model, cell in
let countryCell = cell as! CountryDialTableViewCell
countryCell.configure(model)
}
.disposed(by: disposeBag)

Related

How to clear datas on RxTableView

I'm stuck on RxCocoa problem.
I'm gonna implement clear tableView with Rx.
The app using MVVM with RxCocoa needs clear data for initializing tableView with infinite scroll.
But with binding tableView, I dunno how to clear it.
Thanks.
ViewController
self.viewModel.requestData() // request data to Server
self.viewModel.output.hotDealList
.scan(into: [ItemModel]()) { firstPosts, afterPosts in // For Infinite Scroll
return firstPosts.append(contentsOf: afterPosts)
}
.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}.disposed(by: self.bag)
ViewModel
struct Output {
let hotDealList = BehaviorSubject<[ItemModel]>(value: [])
}
func requestData(page: String = "0") {
let _ = self.service.requestItemList(["page":page])
.subscribe(
onNext:{ response in
guard let serverModels = response.posts, !serverModels.isEmpty else {
return
}
self.output.hotDealList.onNext(serverModels)
}
).disposed(by: self.bag)
}
The solution here is to expand the state machine that you already have started. A Moore Machine (which is the easiest state machine to understand) consists of a number of inputs, a state, a start state, and a number of outputs. It is expressed in Rx using the scan operator and an Input enum.
You already have the scan operator setup, but you only have one input, hotDealList. You need to include a second input for clearing.
Something like this:
enum Input {
case append([ItemModel])
case clear
}
let state = Observable.merge(
viewModel.output.hotDealList.map { Input.append($0) },
viewModel.output.clear.map { Input.clear }
)
.scan(into: [ItemModel]()) { state, input in
switch input {
case let .append(page):
state.append(page)
case .clear:
state = []
}
}
In Rx, the outputs of the state machine are expressed by bindings. You already have one:
state.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}
.disposed(by: bag)
If you need more, be sure to share your state observable.
BTW, using self inside the binder like that is a memory leak. I suggest you move the setCellUI(item:cell:) method into the HotDealTableViewCell class so you don't need self.

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.

Filtering a collectionView with a searchBar from a RxSwift Variable

I've just started implementing RxSwift.
I've got the following function to dynamically fill a collectionView with users returned from a Firebase observe call, but I'm struggling to then dynamically filter the users based on any potential entires in the searchBar.
Rx collectionView binding:
private func bind() {
viewModel.users.asObservable()
.bind(to: nearbyCollectionView.rx.items(cellIdentifier: "NearbyCell", cellType: NearbyCell.self)) {
row, user, cell in
cell.configureCell(user: user)
}.disposed(by: disposeBag)
}
Should I return to the default collectionView implementations and simply use Rx to dynamically update the collectionView objects, or is there a better way to do this?
My old implementation used the following:
if self.viewModel.inSearchMode {
user = self.viewModel.filteredUsers[indexPath.row]
cell.configureCell(user: user)
} else {
user = self.viewModel.users[indexPath.row]
cell.configureCell(user: user)
}
Thanks a lot for any help!!
You can use combineLatest to filter them out
let searchString = searchTextField.rx.text
let filteredUsersObservable = Observable.combineLatest(searchString, viewModel.users, resultSelector: { string, users in
return users.filter { $0 == string }
})
filteredUsersObservable
.bind(to: nearbyCollectionView.rx.items(cellIdentifier: "NearbyCell", cellType: NearbyCell.self)) {
row, user, cell in
cell.configureCell(user: user)
}.disposed(by: disposeBag)
I'm not sure if the syntax fully correct, but the idea is to get the signal every time there's a change on the text field, make it as an observable and filter with the users observable.

RxSwift obtain value from one item in Observable sequence

I'm trying to gradually convert my App to RxSwift / MVVM. But I think I'm doing some things incorrectly.
In this example I have a static table with this specific information.
let itens = Observable.just([
MenuItem(name: GlobalStrings.menuItemHome, nameClass: "GPMainVC"),
MenuItem(name: GlobalStrings.menuItemProfile, nameClass: "GPMainVC"),
MenuItem(name: GlobalStrings.menuItemLevels, nameClass: "GPLevelsVC"),
])
I need to know the model(MenuItem) and the index when the user select a cell, but I am having trouble doing that
tableView.rx
.itemSelected
.map { [weak self] indexPath in
return (indexPath, self?.modelView.itens.elementAt(indexPath.row))
}
.subscribe(onNext: { [weak self] indexPath, model in
self?.tableView.reloadData()
//canĀ“t get MenuItem because model its a observable
//self?.didSelect((indexPath as NSIndexPath).row, name.nameClass)
})
.addDisposableTo(disposeBag)
Thanks in advance
You have to do next steps:
Use Variable. I think it's a better solution in your situation.
let itens = Variable([
MenuItem(name: GlobalStrings.menuItemHome, nameClass: "GPMainVC"),
MenuItem(name: GlobalStrings.menuItemProfile, nameClass: "GPMainVC"),
MenuItem(name: GlobalStrings.menuItemLevels, nameClass: "GPLevelsVC"),
])
Use the following code if you want to get index and model from a clicked cell.
tableView.rx
.itemSelected
.map { index in
return (index, self.items.value[index.row])
}
.subscribe(onNext: { [weak self] index, model in
// model is MenuItem class
})
.addDisposableTo(disposeBag)
I hope my answer was very helpful for you. Please let me know if you want more information about RxSwift opportunities in your task. Good luck!

Resources