web-dev-qa-db-ja.com

RxSwiftを使用したViewModelへのUITextFieldバインディング

モデル値とビューコントローラー間のMVVMバインディングにRxSwiftを使用したいと思います。私はこれに従うことを望んでいました realm.ioチュートリアル ですが、それ以来バインディングは明らかに変化しており、サンプルコードはコンパイルされません。以下にサンプルコードを示します。ここでは、最悪のタイプミス/不足事項を修正したと思います。

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

コントローラのバインディングはコンパイルされません。 RxSwiftのドキュメントは役に立たず、Xcodeのオートコンプリートは有用なものを示唆していないため、これらを行う正しい方法を追跡することはほぼ不可能です。

最初の問題は、次のコンパイルに関するものではありません:usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag)

エラー:

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

私は運なしで以下を試しました:

1)usernameTextField.rx.text.bind(to: viewModel.username).addTo(disposeBag)-エラーは引き続き発生します: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)
_

この2つ目は実際にコンパイルされますが、機能しません(つまり、viewModel.usernameは変更されません)

ここでの主な問題は、bindメソッドと_bind(to:_メソッドにパラメーターを渡すときにブラインドを撮影していることです。ここではオートコンプリートはあまり役に立たないためです。Swift 3を使用していますおよびXcode 8.3.2。

15
satellink

@XFreireはorEmptyが不足している魔法であることは正しいですが、最新の構文に修正されてエラーが修正されたコードがどのように見えるかを知ることは有益かもしれません:

最初にビューモデル...

  • Variable型は常にletで定義する必要があります。 replace変数ではなく、新しいデータを1つにプッシュするだけです。
  • isValidを定義する方法は、バインドまたはサブスクライブするたびに新しいものが作成されます。この単純なケースでは、バインドするのは一度だけなので重要ではありませんが、一般的にはこれは良い習慣ではありません。コンストラクタで一度だけisValidを観測可能にするのが良いでしょう。

Rxを完全に使用する場合、通常、ビューモデルは一連のLetと1つのコンストラクターで構成されます。他のメソッドや計算されたプロパティさえ持っていることは珍しいです。

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

そして、View Controller。繰り返しますが、Rx要素を定義するときはletを使用します。

  • addDisposableTo()は、disposed(by:)の使用よりも推奨されていません
  • bindTo()は、bind(to:)の使用よりも推奨されていません
  • _viewModel.isValid_チェーンにmapは必要ありません。
  • そのチェーンにもdisposed(by:)がありませんでした。

この場合、View Controllerがロードされる前にView Controllerの外部の何かによって割り当てられている場合、実際にviewModelをvarにしたいかもしれません。

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

最後に、ビューモデルを構造体ではなく単一の関数に置き換えることができます。

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

次に、viewDidLoadは次のようになります。

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

このスタイルを使用する場合、一般的なルールは各出力を順番に検討することです。その特定の出力に影響を与えるすべての入力を見つけ、すべての入力をObservableとして受け取り、出力をObservableとして生成する関数を記述します。

52
Daniel T.

.orEmptyを追加する必要があります。

これを試して:

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

... UITextFieldsの残りについても同じ

textプロパティは、String?型のコントロールプロパティです。 orEmptyを追加すると、String?コントロールプロパティをString型のコントロールプロパティに変換します。

15
XFreire