web-dev-qa-db-fra.com

Liaison UITextField à ViewModel avec RxSwift

Je suis prêt à utiliser RxSwift pour la liaison MVVM entre les valeurs de modèle et les contrôleurs de vue. Je voulais suivre ceci tutoriel realm.io , mais la liaison a apparemment changé depuis, et l'exemple de code ne se compile pas. Voici l'exemple de code, où je pense avoir corrigé les pires fautes de frappe/choses manquantes:

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

Les liaisons du contrôleur ne se compilent pas. Il est presque impossible de suivre la bonne façon de le faire, car la documentation de RxSwift est assez inutile et l'auto-complétion Xcode ne suggère rien d'utile.

Le premier problème est avec cette liaison, qui ne compile pas: usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag)

L'erreur:

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

J'ai essayé ce qui suit sans chance:

1) usernameTextField.rx.text.bind(to: viewModel.username).addTo(disposeBag) - L'erreur 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)

Ce second compile en fait, mais ne fonctionne pas (ie. ViewModel.username ne change pas)

Le problème principal est que je tire en aveugle lorsque je passe des paramètres aux méthodes bind et bind(to:, Car la saisie semi-automatique n'est pas vraiment utile ici. J'utilise Swift 3 et Xcode 8.3.2.

15
satellink

@XFreire a raison que orEmpty était la magie manquante, mais il pourrait être instructif pour vous de voir à quoi ressemblerait votre code mis à jour avec la dernière syntaxe et les erreurs corrigées:

D'abord le modèle de vue ...

  • Les types Variable doivent toujours être définis avec let. Vous ne voulez jamais remplacer une variable, vous voulez simplement pousser de nouvelles données en une seule.
  • La façon dont vous avez défini votre isValid, une nouvelle serait créée chaque fois que vous vous y connectez/vous y abonnez. Dans ce cas simple, cela n'a pas d'importance car vous ne vous y connectez qu'une seule fois, mais en général, ce n'est pas une bonne pratique. Il vaut mieux rendre l'observable isValid une seule fois dans le constructeur.

Lorsque vous utilisez pleinement Rx, vous constaterez généralement que vos modèles de vue se composent d'un groupe de let et d'un constructeur unique. Il est inhabituel d'avoir d'autres méthodes ou même des propriétés calculées.

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

Et le contrôleur de vue. Encore une fois, utilisez let lors de la définition des éléments Rx.

  • addDisposableTo() a été déconseillé de préférence à l'utilisation de disposed(by:)
  • bindTo() a été déconseillé de préférence à l'utilisation de bind(to:)
  • Vous n'avez pas besoin du map dans votre chaîne viewModel.isValid.
  • Il vous manquait également la disposed(by:) dans cette chaîne.

Dans ce cas, vous souhaiterez peut-être que votre viewModel soit un var s'il est affecté par quelque chose en dehors du contrôleur de vue avant que ce dernier ne soit chargé.

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

Enfin, votre modèle de vue pourrait être remplacé par une seule fonction au lieu d'une structure:

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

Ensuite, votre viewDidLoad ressemblerait à ceci:

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

En utilisant ce style, la règle générale est de considérer chaque sortie tour à tour. Trouvez toutes les entrées qui influencent cette sortie particulière et écrivez une fonction qui prend toutes les entrées comme observables et produit la sortie comme observable.

52
Daniel T.

Vous devez ajouter .orEmpty.

Essaye ça:

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

... et la même chose pour le reste de vos UITextFields

La propriété text est une propriété de contrôle de type String?. En ajoutant orEmpty vous transformez votre String? propriété de contrôle dans la propriété de contrôle de type String.

15
XFreire