How to bind value property to the button visibility? - ios

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.

Related

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.

do(onNext:) called twice when table view row is selected

I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!

How to change isEnabled and alpha properties of UIButton base on validation using RXSwift?

This is what I simply try to achieve using:
let observable = Observable.combineLatest(loginProperty.asObservable(), passwordProperty.asObservable()) { _,_ in
self.viewModel.isValid
}
observable.bind(to: mainView.loginButton.rx.isEnabled).disposed(by: disposeBag)
//observable.bind(to: mainView.loginButton.rx.alpha).disposed(by: disposeBag)
But this can be bound only to isEnabled property. I would like to also change alpha of my loginButton. How can I do that?
You have to write Reactive extension to bind the property.
extension Reactive where Base : UIButton {
public var valid : Binder<Bool> {
return Binder(self.base) { button, valid in
button.isEnabled = valid
button.alpha = 0.5 // You Value as per valid or not
}
}
}
Then write like this to check the Observable value
observable.bind(to: mainView.loginButton.rx.valid).disposed(by: disposeBag)

RxSwift computed Variable combined from 2 other Variables

I need to have var isOn: Variable<Bool> that is computed from 2 other Variables. How can I subscribe to value changes of the other Variables while being able to read latest value from my isOn Variable as well?
Below is the bad solution using 2 variables of type Observable and Variable. But I am looking for the correct solution, basically to merge my isOn and isOnVariable into single one.
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
var isOn: Observable<Bool> {
return Observable.combineLatest(from.asObservable(), to.asObservable()) { [weak self] to, from in
switch (from, to) {
case (.none, .none):
self?.isOnVariable.value = false
return false
default:
self?.isOnVariable.value = true
return true
}
}
}
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
Observable.merge(from.asObservable(), to.asObservable())
.map { [weak self] (_) -> Void in
guard let `self` = self else {
return
}
switch (self.from.value, self.to.value) {
case (.none, .none):
self.isOnVariable.value = false
default:
self.isOnVariable.value = true
}
}
.subscribe()
.disposed(by: disposeBag)
Now you can subscribe to isOnVariable an listen for value changes, and in any moment you can access values from the from and to variable. I'm not sure what you are trying to do, so you can just change logics in the .map but you got the idea.
If you are interested in why I'm using weak self this is a good reading http://adamborek.com/memory-managment-rxswift/
While working with #markoaras's answer, there is another option to use combineLatest and bind it to isOn Variable. It follows the same principle.
let isFromOpen = Variable<Bool>(false)
let isToOpen = Variable<Bool>(false)
let isOn = Variable<Bool>(false)
Observable.combineLatest(from.asObservable(), to.asObservable()).map{ (from, to) -> Bool in
return from != nil || to != nil
}.bind(to: isOn).disposed(by: bag)

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