Github Signup

Github Signup(示例) - 图1

这是一个模拟用户注册的程序,你可以在这里下载这个例子


简介

这个 App 主要有这样几个交互:

  • 当用户输入户名时,验证用户名是否有效,是否已被占用,将验证结果显示出来。
  • 当用户输入密码时,验证密码是否有效,将验证结果显示出来。
  • 当用户输入重复密码时,验证重复密码是否相同,将验证结果显示出来。
  • 当所有验证都有效时,注册按钮才可点击。
  • 当点击注册按钮后发起注册请求(模拟),然后将结果显示出来。

Service

Github Signup(示例) - 图2

  1. // GitHub 网络服务
  2. protocol GitHubAPI {
  3. func usernameAvailable(_ username: String) -> Observable<Bool>
  4. func signup(_ username: String, password: String) -> Observable<Bool>
  5. }
  6. // 输入验证服务
  7. protocol GitHubValidationService {
  8. func validateUsername(_ username: String) -> Observable<ValidationResult>
  9. func validatePassword(_ password: String) -> ValidationResult
  10. func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult
  11. }
  12. // 弹框服务
  13. protocol Wireframe {
  14. func open(url: URL)
  15. func promptFor<Action: CustomStringConvertible>(_ message: String, cancelAction: Action, actions: [Action]) -> Observable<Action>
  16. }

这里需要集成三个服务:

  • GitHubAPI 提供 GitHub 网络服务
  • GitHubValidationService 提供输入验证服务
  • Wireframe 提供弹框服务

这个例子目前只提供了这三个服务,实际上这一层还可以包含其他的一些服务,例如:数据库,定位,蓝牙…


ViewModel

Github Signup(示例) - 图3

ViewModel 需要集成这些服务,并且将用户输入,转换为状态输出:

  1. class GithubSignupViewModel1 {
  2. // 输出
  3. let validatedUsername: Observable<ValidationResult>
  4. let validatedPassword: Observable<ValidationResult>
  5. let validatedPasswordRepeated: Observable<ValidationResult>
  6. let signupEnabled: Observable<Bool>
  7. let signedIn: Observable<Bool>
  8. let signingIn: Observable<Bool>
  9. // 输入 -> 输出
  10. init(input: ( // 输入
  11. username: Observable<String>,
  12. password: Observable<String>,
  13. repeatedPassword: Observable<String>,
  14. loginTaps: Observable<Void>
  15. ),
  16. dependency: ( // 服务
  17. API: GitHubAPI,
  18. validationService: GitHubValidationService,
  19. wireframe: Wireframe
  20. )
  21. ) {
  22. ...
  23. validatedUsername = ...
  24. validatedPassword = ...
  25. validatedPasswordRepeated = ...
  26. ...
  27. self.signingIn = ...
  28. ...
  29. signedIn = ...
  30. signupEnabled = ...
  31. }
  32. }

集成服务:

  • API GitHub 网络服务
  • validationService 输入验证服务
  • wireframe 弹框服务

输入:

  • username 输入的用户名
  • password 输入的密码
  • repeatedPassword 重复输入的密码
  • loginTaps 点击登录按钮

输出:

  • validatedUsername 用户名校验结果
  • validatedPassword 密码校验结果
  • validatedPasswordRepeated 重复密码校验结果
  • signupEnabled 是否允许登录
  • signedIn 登录结果
  • signingIn 是否正在登录

init 方法内部,将输入转换为输出。


ViewController

Github Signup(示例) - 图4

ViewController 主要负责数据绑定:

  1. ...
  2. class GitHubSignupViewController1 : ViewController {
  3. @IBOutlet weak var usernameOutlet: UITextField!
  4. @IBOutlet weak var usernameValidationOutlet: UILabel!
  5. @IBOutlet weak var passwordOutlet: UITextField!
  6. @IBOutlet weak var passwordValidationOutlet: UILabel!
  7. @IBOutlet weak var repeatedPasswordOutlet: UITextField!
  8. @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!
  9. @IBOutlet weak var signupOutlet: UIButton!
  10. @IBOutlet weak var signingUpOulet: UIActivityIndicatorView!
  11. override func viewDidLoad() {
  12. super.viewDidLoad()
  13. let viewModel = GithubSignupViewModel1(
  14. input: (
  15. username: usernameOutlet.rx.text.orEmpty.asObservable(),
  16. password: passwordOutlet.rx.text.orEmpty.asObservable(),
  17. repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),
  18. loginTaps: signupOutlet.rx.tap.asObservable()
  19. ),
  20. dependency: (
  21. API: GitHubDefaultAPI.sharedAPI,
  22. validationService: GitHubDefaultValidationService.sharedValidationService,
  23. wireframe: DefaultWireframe.shared
  24. )
  25. )
  26. // bind results to {
  27. viewModel.signupEnabled
  28. .subscribe(onNext: { [weak self] valid in
  29. self?.signupOutlet.isEnabled = valid
  30. self?.signupOutlet.alpha = valid ? 1.0 : 0.5
  31. })
  32. .disposed(by: disposeBag)
  33. viewModel.validatedUsername
  34. .bind(to: usernameValidationOutlet.rx.validationResult)
  35. .disposed(by: disposeBag)
  36. viewModel.validatedPassword
  37. .bind(to: passwordValidationOutlet.rx.validationResult)
  38. .disposed(by: disposeBag)
  39. viewModel.validatedPasswordRepeated
  40. .bind(to: repeatedPasswordValidationOutlet.rx.validationResult)
  41. .disposed(by: disposeBag)
  42. viewModel.signingIn
  43. .bind(to: signingUpOulet.rx.isAnimating)
  44. .disposed(by: disposeBag)
  45. viewModel.signedIn
  46. .subscribe(onNext: { signedIn in
  47. print("User signed in \(signedIn)")
  48. })
  49. .disposed(by: disposeBag)
  50. //}
  51. let tapBackground = UITapGestureRecognizer()
  52. tapBackground.rx.event
  53. .subscribe(onNext: { [weak self] _ in
  54. self?.view.endEditing(true)
  55. })
  56. .disposed(by: disposeBag)
  57. view.addGestureRecognizer(tapBackground)
  58. }
  59. }

用户行为传入给 ViewModel

  • username 将用户名输入框的当前文本传入
  • password 将密码输入框的当前文本传入

ViewModel输出状态显示出来:

  • validatedUsername 用对应的 label 将用户名验证结果显示出来
  • validatedPassword 用对应的 label 将密码验证结果显示出来

整体结构

以下是全部的核心代码:

  1. // ViewModel
  2. class GithubSignupViewModel1 {
  3. // outputs {
  4. let validatedUsername: Observable<ValidationResult>
  5. let validatedPassword: Observable<ValidationResult>
  6. let validatedPasswordRepeated: Observable<ValidationResult>
  7. // Is signup button enabled
  8. let signupEnabled: Observable<Bool>
  9. // Has user signed in
  10. let signedIn: Observable<Bool>
  11. // Is signing process in progress
  12. let signingIn: Observable<Bool>
  13. // }
  14. init(input: (
  15. username: Observable<String>,
  16. password: Observable<String>,
  17. repeatedPassword: Observable<String>,
  18. loginTaps: Observable<Void>
  19. ),
  20. dependency: (
  21. API: GitHubAPI,
  22. validationService: GitHubValidationService,
  23. wireframe: Wireframe
  24. )
  25. ) {
  26. let API = dependency.API
  27. let validationService = dependency.validationService
  28. let wireframe = dependency.wireframe
  29. /**
  30. Notice how no subscribe call is being made.
  31. Everything is just a definition.
  32. Pure transformation of input sequences to output sequences.
  33. */
  34. validatedUsername = input.username
  35. .flatMapLatest { username in
  36. return validationService.validateUsername(username)
  37. .observeOn(MainScheduler.instance)
  38. .catchErrorJustReturn(.failed(message: "Error contacting server"))
  39. }
  40. .share(replay: 1)
  41. validatedPassword = input.password
  42. .map { password in
  43. return validationService.validatePassword(password)
  44. }
  45. .share(replay: 1)
  46. validatedPasswordRepeated = Observable.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword)
  47. .share(replay: 1)
  48. let signingIn = ActivityIndicator()
  49. self.signingIn = signingIn.asObservable()
  50. let usernameAndPassword = Observable.combineLatest(input.username, input.password) { ($0, $1) }
  51. signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
  52. .flatMapLatest { (username, password) in
  53. return API.signup(username, password: password)
  54. .observeOn(MainScheduler.instance)
  55. .catchErrorJustReturn(false)
  56. .trackActivity(signingIn)
  57. }
  58. .flatMapLatest { loggedIn -> Observable<Bool> in
  59. let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
  60. return wireframe.promptFor(message, cancelAction: "OK", actions: [])
  61. // propagate original value
  62. .map { _ in
  63. loggedIn
  64. }
  65. }
  66. .share(replay: 1)
  67. signupEnabled = Observable.combineLatest(
  68. validatedUsername,
  69. validatedPassword,
  70. validatedPasswordRepeated,
  71. signingIn.asObservable()
  72. ) { username, password, repeatPassword, signingIn in
  73. username.isValid &&
  74. password.isValid &&
  75. repeatPassword.isValid &&
  76. !signingIn
  77. }
  78. .distinctUntilChanged()
  79. .share(replay: 1)
  80. }
  81. }
  82. // ViewController
  83. class GitHubSignupViewController1 : ViewController {
  84. @IBOutlet weak var usernameOutlet: UITextField!
  85. @IBOutlet weak var usernameValidationOutlet: UILabel!
  86. @IBOutlet weak var passwordOutlet: UITextField!
  87. @IBOutlet weak var passwordValidationOutlet: UILabel!
  88. @IBOutlet weak var repeatedPasswordOutlet: UITextField!
  89. @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!
  90. @IBOutlet weak var signupOutlet: UIButton!
  91. @IBOutlet weak var signingUpOulet: UIActivityIndicatorView!
  92. override func viewDidLoad() {
  93. super.viewDidLoad()
  94. let viewModel = GithubSignupViewModel1(
  95. input: (
  96. username: usernameOutlet.rx.text.orEmpty.asObservable(),
  97. password: passwordOutlet.rx.text.orEmpty.asObservable(),
  98. repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),
  99. loginTaps: signupOutlet.rx.tap.asObservable()
  100. ),
  101. dependency: (
  102. API: GitHubDefaultAPI.sharedAPI,
  103. validationService: GitHubDefaultValidationService.sharedValidationService,
  104. wireframe: DefaultWireframe.shared
  105. )
  106. )
  107. // bind results to {
  108. viewModel.signupEnabled
  109. .subscribe(onNext: { [weak self] valid in
  110. self?.signupOutlet.isEnabled = valid
  111. self?.signupOutlet.alpha = valid ? 1.0 : 0.5
  112. })
  113. .disposed(by: disposeBag)
  114. viewModel.validatedUsername
  115. .bind(to: usernameValidationOutlet.rx.validationResult)
  116. .disposed(by: disposeBag)
  117. viewModel.validatedPassword
  118. .bind(to: passwordValidationOutlet.rx.validationResult)
  119. .disposed(by: disposeBag)
  120. viewModel.validatedPasswordRepeated
  121. .bind(to: repeatedPasswordValidationOutlet.rx.validationResult)
  122. .disposed(by: disposeBag)
  123. viewModel.signingIn
  124. .bind(to: signingUpOulet.rx.isAnimating)
  125. .disposed(by: disposeBag)
  126. viewModel.signedIn
  127. .subscribe(onNext: { signedIn in
  128. print("User signed in \(signedIn)")
  129. })
  130. .disposed(by: disposeBag)
  131. //}
  132. let tapBackground = UITapGestureRecognizer()
  133. tapBackground.rx.event
  134. .subscribe(onNext: { [weak self] _ in
  135. self?.view.endEditing(true)
  136. })
  137. .disposed(by: disposeBag)
  138. view.addGestureRecognizer(tapBackground)
  139. }
  140. }

这里还有一个 Driver 版的演示代码,有兴趣的同学可以了解一下。