UITextField binding to ViewModel with RxSwift
@XFreire is right that orEmpty
was the missing magic, but it might be instructive for you to see what your code would look like updated to the latest syntax and errors corrected:
First the view model...
Variable
types should always be defined withlet
. You don't want to ever replace a Variable, you just want to push new data into one.- The way you have your
isValid
defined, a new one would be created every time you bind/subscribe to it. In this simple case that doesn't matter because you only bind to it once, but in general, this is not good practice. Better is to make the isValid observable just once in the constructor.
When using Rx fully, you will usually find that your view models consist of a bunch of let's and a single constructor. It's unusual to have any other methods or even computed properties.
struct LoginViewModel {
let username = Variable<String>("")
let password = Variable<String>("")
let isValid: Observable<Bool>
init() {
isValid = Observable.combineLatest(self.username.asObservable(), self.password.asObservable())
{ (username, password) in
return username.characters.count > 0
&& password.characters.count > 0
}
}
}
And the view controller. Again, use let
when defining Rx elements.
addDisposableTo()
has been deprecated in preference to usingdisposed(by:)
bindTo()
has been deprecated in preference to usingbind(to:)
- You don't need the
map
in yourviewModel.isValid
chain. - You were missing the
disposed(by:)
in that chain as well.
In this case, you might actually want your viewModel
to be a var if it is assigned by something outside the view controller before the latter is loaded.
class LoginViewController: UIViewController {
var usernameTextField: UITextField!
var passwordTextField: UITextField!
var confirmButton: UIButton!
let viewModel = LoginViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
usernameTextField.rx.text
.orEmpty
.bind(to: viewModel.username)
.disposed(by: disposeBag)
passwordTextField.rx.text
.orEmpty
.bind(to: viewModel.password)
.disposed(by: disposeBag)
//from the viewModel
viewModel.isValid
.bind(to: confirmButton.rx.isEnabled)
.disposed(by: disposeBag)
}
}
Lastly, your view model could be replaced by a single function instead of a struct:
func confirmButtonValid(username: Observable<String>, password: Observable<String>) -> Observable<Bool> {
return Observable.combineLatest(username, password)
{ (username, password) in
return username.characters.count > 0
&& password.characters.count > 0
}
}
Then your viewDidLoad would look like this:
override func viewDidLoad() {
super.viewDidLoad()
let username = usernameTextField.rx.text.orEmpty.asObservable()
let password = passwordTextField.rx.text.orEmpty.asObservable()
confirmButtonValid(username: username, password: password)
.bind(to: confirmButton.rx.isEnabled)
.disposed(by: disposeBag)
}
Using this style, the general rule is to consider each output in turn. Find all the inputs that influence that particular output and write a function that takes all the inputs as Observables and produces the output as an Observable.
You should add .orEmpty
.
Try this:
usernameTextField.rx.text
.orEmpty
.bindTo(self.viewModel. username)
.addDisposableTo(disposeBag)
... and the same for the rest of your UITextField
s
The text
property is a control property of type String?
. Adding orEmpty
you transform your String?
control property into control property of type String
.