UITextField привязывается к ViewModel с помощью RxSwift

Я готов использовать RxSwift для привязки MVVM между значениями модели и контроллерами представления. Я хотел следовать этому руководству по 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) с этой привязкой, которая не компилируется: 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)

Этот второй фактически компилируется, но не работает (т.е. viewModel.username не изменяется)

Основная проблема заключается в том, что я стреляю вслепую при передаче параметров в bind и bind(to: методы, так как автодополнение здесь не очень полезно... Я использую swift 3 и Xcode 8.3.2.

Ответ 1

@XFreire прав в том, что orEmpty был отсутствующей магией, но вам может быть orEmpty посмотреть, как будет выглядеть ваш код, обновленный до последнего синтаксиса, и исправленные ошибки:

Сначала модель представления...

  • Типы Variable всегда должны быть определены с помощью let. Вы не хотите когда-либо заменять переменную, вы просто хотите вставить новые данные в одну.
  • Как вы определили свой isValid, новый будет создаваться каждый раз, когда вы привязываете/подписываетесь на него. В этом простом случае это не имеет значения, потому что вы привязываетесь к нему только один раз, но в целом это не очень хорошая практика. Лучше сделать isValid наблюдаемым только один раз в конструкторе.

При полном использовании Rx вы обычно обнаружите, что ваши модели представлений состоят из набора let и одного конструктора. Это необычно иметь любые другие методы или даже вычисляемые свойства.

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

И контроллер просмотра. Опять же, используйте let при определении элементов Rx.

  • addDisposableTo() устарел из-за предпочтения использования disposed(by:)
  • bindTo() устарел из-за предпочтения использования bind(to:)
  • Вам не нужна map в вашей цепочке viewModel.isValid.
  • Вы пропустили disposed(by:) в этой цепочке.

В этом случае вы могли бы фактически захотеть, чтобы ваш 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)
}

Используя этот стиль, общее правило состоит в том, чтобы рассматривать каждый вывод по очереди. Найдите все входные данные, которые влияют на этот конкретный выход, и напишите функцию, которая принимает все входные данные как наблюдаемые и производит выходные данные как наблюдаемые.

Ответ 2

Вы должны добавить .orEmpty.

Попробуйте следующее:

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

... и то же самое для остальных ваших UITextField s

Свойство text является управляющим свойством типа String?. Добавляя orEmpty, вы преобразуете свойство управления String? в свойство управления типа String.