# 2. 你好 RxSwift！

## 我的第一个 RxSwift 应用程序 - 输入验证：

这是一个模拟用户登录的程序。

* 当用户输入用户名时，如果用户名不足 5 个字就给出红色提示语，并且无法输入密码，当用户名符合要求时才可以输入密码。
* 同样的当用户输入的密码不到 5 个字时也给出红色提示语。
* 当用户名和密码有一个不符合要求时底部的绿色按钮不可点击，只有当用户名和密码同时有效时按钮才可点击。
* 当点击绿色按钮后弹出一个提示框，这个提示框只是用来做演示而已。

你可以下载[这个例子](https://github.com/ReactiveX/RxSwift/tree/master/RxExample/RxExample/Examples/SimpleValidation)并在模拟器上运行，这样可以帮助于你理解整个程序的交互：

![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2F89eb44bd16e7eceac3b6ba1491b82d56fc69d1f9.gif?generation=1616819394189653\&alt=media)

### 这个页面主要由 5 各元素组成：

1. 用户名输入框
2. 用户名提示语（红色）
3. 密码输入框
4. 密码提示语（红色）
5. 操作按钮（绿色）

```swift
class SimpleValidationViewController : ViewController {

    @IBOutlet weak var usernameOutlet: UITextField!
    @IBOutlet weak var usernameValidOutlet: UILabel!

    @IBOutlet weak var passwordOutlet: UITextField!
    @IBOutlet weak var passwordValidOutlet: UILabel!

    @IBOutlet weak var doSomethingOutlet: UIButton!
    ...
}
```

### 这里需要完成 4 个交互：

* 当用户名输入不到 5 个字时显示提示语，并且无法输入密码

  ![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2Fcceb9241125213edcd791b5f870a1901226238c0.png?generation=1616819397173191\&alt=media)

  ```swift
  override func viewDidLoad() {
      super.viewDidLoad()

      ...

      // 用户名是否有效
      let usernameValid = usernameOutlet.rx.text.orEmpty
          // 用户名 -> 用户名是否有效
          .map { $0.count >= minimalUsernameLength }
          .share(replay: 1)

      ...

      // 用户名是否有效 -> 密码输入框是否可用
      usernameValid
          .bind(to: passwordOutlet.rx.isEnabled)  
          .disposed(by: disposeBag)

      // 用户名是否有效 -> 用户名提示语是否隐藏
      usernameValid
          .bind(to: usernameValidOutlet.rx.isHidden)
          .disposed(by: disposeBag)

      ...
  }
  ```

  当用户修改用户名输入框的内容时就会产生一个新的用户名, 然后通过 `map` 方法将它转化成用户名是否有效, 最后通过 `bind(to: ...)` 来决定密码输入框是否可用以及提示语是否隐藏。
* 当密码输入不到 5 个字时显示提示文字

  ![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2F2ea62d99ede67a02a99ee538b02844de34cb4b29.png?generation=1616819382555989\&alt=media)

  ```swift
  override func viewDidLoad() {
      super.viewDidLoad()

      ...

      // 密码是否有效
      let passwordValid = passwordOutlet.rx.text.orEmpty
          // 密码 -> 密码是否有效
          .map { $0.count >= minimalPasswordLength }
          .share(replay: 1)

      ...

      // 密码是否有效 -> 密码提示语是否隐藏
      passwordValid
          .bind(to: passwordValidOutlet.rx.isHidden)
          .disposed(by: disposeBag)

      ...
  }
  ```

  这个和用用户名来控制提示语的逻辑是一样的。
* 当用户名和密码都符合要求时，绿色按钮才可点击

  ![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2F84d1e2a0e0cf92089a0511e643f4fc53c41867b8.png?generation=1616819397221610\&alt=media)

  ```swift
  override func viewDidLoad() {
      super.viewDidLoad()

      ...

      // 用户名是否有效
      let usernameValid = ...

      // 密码是否有效
      let passwordValid = ...

      ...

      // 所有输入是否有效
      let everythingValid = Observable.combineLatest(
            usernameValid,
            passwordValid
          ) { $0 && $1 } // 取用户名和密码同时有效
          .share(replay: 1)

      ...

      // 所有输入是否有效 -> 绿色按钮是否可点击
      everythingValid
          .bind(to: doSomethingOutlet.rx.isEnabled)
          .disposed(by: disposeBag)

      ...
  }
  ```

  通过 `Observable.combineLatest(...) { ... }` 来将`用户名是否有效`以及`密码是都有效`合并出`两者是否同时有效`,然后用它来控制绿色按钮是否可点击。
* 点击绿色按钮后，弹出一个提示框

  ![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2Fec350a3530463772d5a14926ddf10a5cc5d0e668.png?generation=1616819397022450\&alt=media)

  ```swift
  override func viewDidLoad() {
      super.viewDidLoad()

      ...

      // 点击绿色按钮 -> 弹出提示框
      doSomethingOutlet.rx.tap
          .subscribe(onNext: { [weak self] in self?.showAlert() })
          .disposed(by: disposeBag)
  }

  func showAlert() {
      let alertView = UIAlertView(
          title: "RxExample",
          message: "This is wonderful",
          delegate: nil,
          cancelButtonTitle: "OK"
      )

      alertView.show()
  }
  ```

  在点击绿色按钮后，弹出一个提示框

这样 4 个交互都完成了，现在我们纵观全局看下这个程序是一个什么样的结构:

![](https://4217506537-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MWj545abP2yK86-qksJ%2Fsync%2Fc35106c424dc5e5d54ececff10bc0d7b3ecce39f.png?generation=1616819396869596\&alt=media)

然后看一下完整的代码:

```swift
override func viewDidLoad() {
    super.viewDidLoad()

    usernameValidOutlet.text = "Username has to be at least \(minimalUsernameLength) characters"
    passwordValidOutlet.text = "Password has to be at least \(minimalPasswordLength) characters"

    let usernameValid = usernameOutlet.rx.text.orEmpty
        .map { $0.count >= minimalUsernameLength }
        .share(replay: 1)

    let passwordValid = passwordOutlet.rx.text.orEmpty
        .map { $0.count >= minimalPasswordLength }
        .share(replay: 1)

    let everythingValid = Observable.combineLatest(
          usernameValid,
          passwordValid
        ) { $0 && $1 }
        .share(replay: 1)

    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    usernameValid
        .bind(to: usernameValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    passwordValid
        .bind(to: passwordValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    everythingValid
        .bind(to: doSomethingOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    doSomethingOutlet.rx.tap
        .subscribe(onNext: { [weak self] in self?.showAlert() })
        .disposed(by: disposeBag)
}

func showAlert() {
    let alertView = UIAlertView(
        title: "RxExample",
        message: "This is wonderful",
        delegate: nil,
        cancelButtonTitle: "OK"
    )

    alertView.show()
}
```

你会发现你可以用几行代码完成如此复杂的交互。这可以大大提升我们的开发效率。

### 更多疑问

* `share(replay: 1)` 是用来做什么的？

  我们用 `usernameValid` 来控制用户名提示语是否隐藏以及密码输入框是否可用。[shareReplay](https://github.com/adong666666/RxSwiftDoc/tree/2719ead6843f9738f474b4f02d3ca7a06085a383/content/rxswift_core/operator/shareReplay.md) 就是让他们共享这一个源，而不是为他们单独创建新的源。这样可以减少不必要的开支。
* `disposed(by: disposeBag)` 是用来做什么的？

  和我们所熟悉的对象一样，每一个绑定也是有生命周期的。并且这个绑定是可以被清除的。`disposed(by: disposeBag)`就是将绑定的生命周期交给 `disposeBag` 来管理。当 `disposeBag` 被释放的时候，那么里面尚未清除的绑定也就被清除了。这就相当于是在用 [ARC](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID48) 来管理绑定的生命周期。 这个内容会在 [Disposable](https://3440217568.gitbook.io/rxswift/rxswift_core/disposable) 章节详细介绍。
