RxSwift How to add "clear all" UIButton to simple spreadsheet example - ios

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.

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.

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.

How to bind value property to the button visibility?

I need to style UIButton depends on values from two textFields:
Observable.combineLatest(loginProperty.asObservable(), passwordProperty.asObservable()) { _, _ in
self.viewModel.isValid
}.bind(to: mainView.loginButton.rx.isValid).disposed(by: bag)
Also I need to style UIButton depends on value from one text field, and above solution doesn't work. Why? What is the simple way to do that?
You can use combinedLatest to combine email and password to create isButtonEnabled Observable and bind the values to BehaviorRelay, then subscribe to onNext events from the BehaviorRelay and set values
func rxLogin() {
let isValidPassword = username.rx.text.orEmpty
.map { $0.count > 8 }
.distinctUntilChanged()
let isValidEmail = password.rx.text.orEmpty
.map { $0.contains("#") }
.distinctUntilChanged()
let isButtonEnabled: Observable<Bool> = Observable.combineLatest(isValidEmail, isValidPassword) { $0 && $1 }.share()
//option 1
let submitButtonState: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
isButtonEnabled
.bind(to: submitButtonState)
.disposed(by: disposeBag)
submitButtonState
.bind { (isEnabled) in
self.loginButton.isEnabled = isEnabled
self.loginButton.backgroundColor = isEnabled ? UIColor.green : UIColor.red
}.disposed(by: disposeBag)
//OR
isButtonEnabled
.subscribe(onNext: { (isValidCredentials) in
self.loginButton.isEnabled = isValidCredentials
self.loginButton.backgroundColor = isValidCredentials ? .green : .red
}).disposed(by: disposeBag)
}
As Al_ mentioned in his comment, combineLatest will emit first next value when both streams will emit at least one value.
To style button depending on one of the text field rx properties:
you can use side effects (ex: .do(onNext: { ... }))
or just make another subscription to the property
Are you using any extension? As there is no property isValid, try to use mainView.loginButton.rx.isHidden if you want to change visibility of the button.

RxSwift Build an Observable based on a Variable

I am trying to build an Observable which would output a value based on the value of a Variable.
Something like that:
let fullName = Variable<String>("")
let isFullNameOKObs: Observable<Bool>
isFullNameOKObs = fullName
.asObservable()
.map { (val) -> Bool in
// here business code to determine if the fullName is 'OK'
let ok = val.characters.count >= 3
return ok
}
Unfortunately the bloc in the map func is never called!
The reason behind this is that:
The fullName Variable is binded to a UITextField with the bidirectional operator <-> as defined in the RxSwift example.
The isFullNameOKObs Observable will be observed to hide or display the submit button of my ViewController.
Any help would be greatly appreciated.
Thanks
The model
class Model {
let fullName = Variable<String>("")
let isFullNameOKObs: Observable<Bool>
let disposeBag = DisposeBag()
init(){
isFullNameOKObs = fullName
.asObservable()
.debug("isFullNameOKObs")
.map { (val) -> Bool in
let ok = val.characters.count >= 3
return ok
}
.debug("isFullNameOKObs")
isRegFormOKObs = Observable.combineLatest(
isFullNameOKObs,
is...OK,
... ) { $0 && $1 && ... }
isRegFormOKObs
.debug("isRegFormOKObs")
.asObservable()
.subscribe { (event) in
// update the OK button
}
// removing this disposedBy resolved the problem
//.disposed(by: DisposeBag())
}
}
The ViewController:
func bindModel() -> Void {
_ = txFullName.rx.textInput <-> model!.fullName
......
}
Do you need the two-way binding between the UITextField and your Variable?
If not, I would suggest you try to just use bindTo() instead like this:
myTextField.rx.text.orEmpty.bindTo(fullName).disposed(by: disposeBag)

RxSwift: code working only first time

I'm new in RxSwift. Some strange thing happens in my code.
I have a collection view and
Driver["String"]
Data for binding.
var items = fetchImages("flower")
items.asObservable().bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)
fetchImages
Function returns data
private func fetchImages(string:String) -> Driver<[String]> {
let searchData = Observable.just(string)
return searchData.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMap
{ text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://pixabay.com/api/?key=2557096-723b632d4f027a1a50018f846&q=\(text)&image_type=photo")
.debug()
.catchError { error in
print("aaaa")
return Observable.never()
}
}
.map { (response, json) -> [String] in // again back to .Background, map objects
var arr = [String]()
for i in 0 ..< json["hits"]!!.count {
arr.append(json["hits"]!![i]["previewURL"]!! as! String)
}
return arr
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOnError({ (type) in
print(type)
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
Strange thing is this. First time when I fetch with "flower" it works and return data, but when I add this code
self.searchBar.rx_text.subscribeNext { text in
items = self.fetchImages(text)
}.addDisposableTo(self.disposeBag)
It doesn't work. It doesn't steps in flatmap callback, and because of this, doesn't return anything.
It works in your first use case, because you're actually using the returned Driver<[String]> via a bindTo():
var items = fetchImages("flower")
items.asObservable().bindTo(...
However, in your second use case, you aren't doing anything with the returned Driver<[String]> other than saving it to a variable, which you do nothing with.
items = self.fetchImages(text)
A Driver does nothing until you subscribe to it (or in your case bindTo).
EDIT: To make this clearer, here's how you could get your second use case to work (I've avoided cleaning up the implementation to keep it simple):
self.searchBar.rx_text
.flatMap { searchText in
return self.fetchImages(searchText)
}
.bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)

Resources