UITextField 使用 RxSwift 绑定到 ViewModel

Posted

技术标签:

【中文标题】UITextField 使用 RxSwift 绑定到 ViewModel【英文标题】:UITextField binding to ViewModel with RxSwift 【发布时间】:2017-12-27 08:14:34 【问题描述】:

我愿意将 RxSwift 用于模型值和视图控制器之间的 MVVM 绑定。我想关注这个realm.io tutorial,但从那时起绑定显然发生了变化,示例代码无法编译。这是示例代码,我认为我已经修复了最严重的错别字/遗漏的东西:

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&lt;String&gt;)'

我尝试了以下方法,但没有运气:

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&lt;String&gt;)'

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

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

第二个实际上编译,但不起作用(即 viewModel.username 没有改变)

这里的主要问题是我在将参数传递给bindbind(to: 方法时会失明,因为自动补全在这里并没有真正的帮助.. 我使用的是 swift 3 和 Xcode 8.3.2。

【问题讨论】:

【参考方案1】:

@XFreire 说得对,orEmpty 是缺少的魔法,但是看看你的代码看起来像更新到最新的语法并纠正了错误可能对你很有启发:

首先是视图模型...

Variable 类型应始终使用 let 定义。您不想替换变量,您只想将新数据推送到一个变量中。 按照您定义isValid 的方式,每次绑定/订阅它时都会创建一个新的。在这种简单的情况下,这并不重要,因为您只绑定一次,但总的来说,这不是一个好习惯。更好的是在构造函数中只使 isValid 可观察一次。

当完全使用 Rx 时,您通常会发现您的视图模型由一堆 let's 和一个构造函数组成。拥有任何其他方法甚至计算属性是不寻常的。

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
        
    

还有视图控制器。同样,在定义 Rx 元素时使用let

addDisposableTo() 已被弃用,优先使用 disposed(by:) bindTo() 已被弃用,优先使用 bind(to:) 您的viewModel.isValid 链中不需要map。 您也缺少该链中的 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)

使用这种风格,一般规则是依次考虑每个输出。找出影响特定输出的所有输入,并编写一个函数,将所有输入作为 Observable 并将输出作为 Observable 生成。

【讨论】:

这真的很有教育意义,谢谢!接下来我必须编写一个提交操作并返回用户的函数。 我也在看这个演讲,Max Alexander 明确提到我们应该摆脱视图模型.. 我不知道这个谈话(链接会很有用!),但 ViewModel 的最大好处是可测试性;你那里没有 UIKit 依赖项,例如。在这种特殊情况下,您可以在 ViewModel 中使用 confirmButtonValid 方法,并对其进行很好的单元测试。 上面的代码并没有摆脱视图模型。它将视图模型变成单个函数或函数集合。函数易于测试。 @Womble 你是对的。这是问题的延续,不需要。【参考方案2】:

你应该添加.orEmpty

试试这个:

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

...UITextFields 的其余部分也是如此

text 属性是String? 类型的控件属性。添加orEmpty 可以将String? 控件属性转换为String 类型的控件属性。

【讨论】:

以上是关于UITextField 使用 RxSwift 绑定到 ViewModel的主要内容,如果未能解决你的问题,请参考以下文章

RxSwift:将 RX 绑定添加到 UITextField 时出错:“Binder<String?>”类型的值没有成员“debounce”

根据 UITextField 中的值禁用按钮只能工作一次(RxSwift)

RxSwift var-outlet 绑定组织

如何使用 RxSwift 观察 UITextField 中的文本变化?

UITextField和一个UILabel绑定 浅析

RxSwift 从一个创建多个 Observable