UITextField binding para ViewModel con RxSwift

Estoy dispuesto a usar RxSwift para vincular MVVM entre los valores del model y los controlleres de vista. Quería seguir este tutorial realm.io , pero el enlace aparentemente ha cambiado desde entonces, y el código de ejemplo no se comstack. Aquí está el código de ejemplo, donde creo que he corregido los peores errores tipocharts / cosas que faltan:

LoginViewModel.swift

import RxSwift struct LoginViewModel { var username = Variable<String>("") var password = Variable<String>("") var isValid : Observable<Bool>{ return Observable.combineLatest(self.username.asObservable(), self.password.asObservable()) { (username, password) in return username.characters.count > 0 && password.characters.count > 0 } } } 

LoginViewController.swift

 import RxSwift import RxCocoa import UIKit class LoginViewController: UIViewController { var usernameTextField: UITextField! var passwordTextField: UITextField! var confirmButton: UIButton! var viewModel = LoginViewModel() var disposeBag = DisposeBag() override func viewDidLoad() { usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag) passwordTextField.rx.text.bindTo(viewModel.password).addTo(disposeBag) //from the viewModel viewModel.rx.isValid.map { $0 } .bindTo(confirmButton.rx.isEnabled) } } 

Los enlaces del controller no se comstackn. Es casi imposible rastrear la forma correcta de hacer esto, ya que la documentation de RxSwift es bastante inútil, y la autocompletado de Xcode no sugiere nada útil.

El primer problema es con este enlace, que no comstack: usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag)

El error:

LoginViewController.swift:15:35: Cannot invoke 'bindTo' with an argument list of type '(Variable<String>)'

He intentado lo siguiente sin suerte:

1) usernameTextField.rx.text.bind(to: viewModel.username).addTo(disposeBag) – El error aún persiste: LoginViewController.swift:15:35: Cannot invoke 'bind' with an argument list of type '(to: Variable<String>)'

2) let _ = viewModel.username.asObservable (). Bind (to: passwordTextField.rx.text)

 let _ = viewModel.username.asObservable() .map { $0 } .bind(to: usernameTextField.rx.text) 

Este segundo realmente comstack, pero no funciona (es decir, viewModel.username no cambia)

El problema principal es que estoy disparando a ciegas al pasar parameters al bind y al bind(to: methods, ya que la autocompletado no es realmente útil aquí. Estoy usando swift 3 y Xcode 8.3.2.

@XFreire tiene razón en que orEmpty era la magia perdida, pero podría ser instructivo que veas cómo se vería tu código actualizado a la última syntax y errores corregidos:

Primero el model de vista …

  • Variable types Variable siempre deben definirse con let . No desea replace alguna vez una Variable, solo desea insert nuevos datos en uno.
  • La forma en que tiene su isValid definido, se isValid una nueva cada vez que se una / se suscriba. En este caso simple que no importa porque solo se unen a él una vez, pero en general, esta no es una buena práctica. Mejor es hacer que el isValid sea observable solo una vez en el constructor.

Cuando utilice Rx por completo, generalmente encontrará que sus models de vista consisten en un montón de let's y un solo constructor. Es raro tener otros methods o incluso properties computadas.

 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 } } } 

Y el controller de vista. Nuevamente, use let al definir los elementos Rx.

  • addDisposableTo() ha quedado en desuso con preference a usar disposed(by:)
  • bindTo() ha quedado en desuso con preference a usar bind(to:)
  • No necesita el map en su viewModel.isValid chain.
  • Estabas perdiendo lo disposed(by:) en esa cadena también.

En este caso, es posible que desee que su viewModel sea ​​una var si está asignado por algo fuera del controller de vista antes de cargarlo.

 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.map { $0 } .bind(to: confirmButton.rx.isEnabled) .disposed(by: disposeBag) } } 

Por último, su model de vista podría replacese por una única function en lugar de una estructura:

 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 } } 

Entonces su viewDidLoad se vería así:

 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) } 

Usando este estilo, la regla general es considerar cada salida sucesivamente. Encuentre todas las inputs que influyen en esa salida particular y escriba una function que tome todas las inputs como observables y produzca la salida como un observable.

Debe agregar .orEmpty .

Prueba esto:

 usernameTextField.rx.text .orEmpty .bindTo(self.viewModel. username) .addDisposableTo(disposeBag) 

… y lo mismo para el rest de su UITextField s

La propiedad de text es una propiedad de control de tipo String? . Agregando o orEmpty transforms tu String? controle la propiedad en propiedad de control de tipo String .