RxSwift obtain value from one item in Observable sequence - ios

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!

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.

How can I update tableView from BehaviorRelay observable?

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)

Deleting item RxSwift MVVM pattern

Hi have a tableview with sections and I am making API call to populate the tableView. I am also using the MVVm architecture. Now users are able to delete items but I try reloading the sections or tableView but nothing happens as the deleted item still remains in the tableView. Below is my code. Any help is appreciated
My ViewModel
Observable.zip(identiferElements, deviceElements).map {(identifers, devices, _) -> [MyInfoSection] in
var items: [MyInfoSection] = []
let identiferRepository = identifers.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = IdentifiersCellViewModel(with: repository)
return MyInfoSectionItem.identifiersItem(viewModel: cellViewModel)
})
if identiferRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Identifier", items: identiferRepository))
}
let deviceRepository = devices.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = DevicesCellViewModel(with: repository)
return MyInfoSectionItem.devicesItem(viewModel: cellViewModel)
})
if deviceRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Active Devices", items: deviceRepository))
}
return items
}.bind(to: elements).disposed(by: rx.disposeBag)
deletedEvent.drive(onNext: { (item) in
switch item {
case .identifiersItem(let viewModel):
identiferDeleted.onNext(viewModel.repository)
case .devicesItem(let viewModel):
deviceDeleted.onNext(viewModel.repository)
}
}).disposed(by: rx.disposeBag)
identiferDeleted.asObservable().flatMapLatest({ [weak self] (value) -> Observable<ResponseBase> in
log(value)
guard let self = self, let id = value.id else { return Observable.just(ResponseBase()) }
return self.provider.deleteAddress(id: id)
.trackActivity(self.loading)
.trackError(self.error)
}).subscribe(onNext: { (res) in
log(res)
}).disposed(by: rx.disposeBag)
ViewController
//viewDidLoad
let input = MyInfoViewModel.Input(trigger: refresh, segmentSelection: segmentSelected, selection: tableView.rx.modelSelected(MyInfoSectionItem.self).asDriver(), deleted: tableView.rx.modelDeleted(MyInfoSectionItem.self).asDriver())
let output = viewModel.transform(input: input)
More code would be added based on request. Thanks
Use combineLatest instead of zip. The user can only delete an item out of one section at a time and zip waits until both sections emit a new value before emitting. There might be other problems, but that is one for sure.
I would need to see compilable code for your view model to help further.

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 How to add "clear all" UIButton to simple spreadsheet example

Here's the code from the RxSwift repo for a simple spreadsheet (adding three numbers):
Observable.combineLatest(number1.rx_text, number2.rx_text, number3.rx_text) { textValue1, textValue2, textValue3 -> Int in
return (Int(textValue1) ?? 0) + (Int(textValue2) ?? 0) + (Int(textValue3) ?? 0)
}
.map { $0.description }
.bindTo(result.rx_text)
.addDisposableTo(disposeBag)
I want to add a "Clear all" UIButton, that will cause the three boxes to be reset to zero and the total also set to zero. In imperative style, very easy.
What is the correct way of adding this button in RxSwift?
Here are a couple of ways off the top of my head.
Imperative
button.rx_tap
.subscribeNext { [weak self] _ in
self?.number1.text = ""
self?.number2.text = ""
self?.number3.text = ""
}
.addDisposableTo(disposeBag)
Note however, that this will not update the combineLatest Observable below until you change focus between fields. I haven't looked into why.
Using Variables
// Declared as a property
let variable1 = Variable<String>("")
// Back down in `viewDidLoad`
number1.rx_text
.bindTo(variable1)
.addDisposableTo(disposeBag)
variable1.asObservable()
.bindTo(number1.rx_text)
.addDisposableTo(disposeBag)
button.rx_tap
.subscribeNext { [weak self] _ in
self?.variable1.value = ""
// and other variables as well..
}
.addDisposableTo(disposeBag)
Observable.combineLatest(variable1.asObservable(), ....
I've omitted showing the same code for variable2 and variable3, as you should probably make a two-way binding helper method if you go this route.

Resources