Github Signup

这是一个模拟用户注册的程序,你可以在这里下载这个例子。
简介
这个 App 主要有这样几个交互:
- 当用户输入户名时,验证用户名是否有效,是否已被占用,将验证结果显示出来。
- 当用户输入密码时,验证密码是否有效,将验证结果显示出来。
- 当用户输入重复密码时,验证重复密码是否相同,将验证结果显示出来。
- 当所有验证都有效时,注册按钮才可点击。
- 当点击注册按钮后发起注册请求(模拟),然后将结果显示出来。
Service

// GitHub 网络服务protocol GitHubAPI {func usernameAvailable(_ username: String) -> Observable<Bool>func signup(_ username: String, password: String) -> Observable<Bool>}// 输入验证服务protocol GitHubValidationService {func validateUsername(_ username: String) -> Observable<ValidationResult>func validatePassword(_ password: String) -> ValidationResultfunc validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult}// 弹框服务protocol Wireframe {func open(url: URL)func promptFor<Action: CustomStringConvertible>(_ message: String, cancelAction: Action, actions: [Action]) -> Observable<Action>}
这里需要集成三个服务:
- GitHubAPI 提供 GitHub 网络服务
- GitHubValidationService 提供输入验证服务
- Wireframe 提供弹框服务
这个例子目前只提供了这三个服务,实际上这一层还可以包含其他的一些服务,例如:数据库,定位,蓝牙…
ViewModel

ViewModel 需要集成这些服务,并且将用户输入,转换为状态输出:
class GithubSignupViewModel1 {// 输出let validatedUsername: Observable<ValidationResult>let validatedPassword: Observable<ValidationResult>let validatedPasswordRepeated: Observable<ValidationResult>let signupEnabled: Observable<Bool>let signedIn: Observable<Bool>let signingIn: Observable<Bool>// 输入 -> 输出init(input: ( // 输入username: Observable<String>,password: Observable<String>,repeatedPassword: Observable<String>,loginTaps: Observable<Void>),dependency: ( // 服务API: GitHubAPI,validationService: GitHubValidationService,wireframe: Wireframe)) {...validatedUsername = ...validatedPassword = ...validatedPasswordRepeated = ......self.signingIn = ......signedIn = ...signupEnabled = ...}}
集成服务:
- API GitHub 网络服务
- validationService 输入验证服务
- wireframe 弹框服务
输入:
- username 输入的用户名
- password 输入的密码
- repeatedPassword 重复输入的密码
- loginTaps 点击登录按钮
输出:
- validatedUsername 用户名校验结果
- validatedPassword 密码校验结果
- validatedPasswordRepeated 重复密码校验结果
- signupEnabled 是否允许登录
- signedIn 登录结果
- signingIn 是否正在登录
在 init 方法内部,将输入转换为输出。
ViewController

ViewController 主要负责数据绑定:
...class GitHubSignupViewController1 : ViewController {@IBOutlet weak var usernameOutlet: UITextField!@IBOutlet weak var usernameValidationOutlet: UILabel!@IBOutlet weak var passwordOutlet: UITextField!@IBOutlet weak var passwordValidationOutlet: UILabel!@IBOutlet weak var repeatedPasswordOutlet: UITextField!@IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!@IBOutlet weak var signupOutlet: UIButton!@IBOutlet weak var signingUpOulet: UIActivityIndicatorView!override func viewDidLoad() {super.viewDidLoad()let viewModel = GithubSignupViewModel1(input: (username: usernameOutlet.rx.text.orEmpty.asObservable(),password: passwordOutlet.rx.text.orEmpty.asObservable(),repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),loginTaps: signupOutlet.rx.tap.asObservable()),dependency: (API: GitHubDefaultAPI.sharedAPI,validationService: GitHubDefaultValidationService.sharedValidationService,wireframe: DefaultWireframe.shared))// bind results to {viewModel.signupEnabled.subscribe(onNext: { [weak self] valid inself?.signupOutlet.isEnabled = validself?.signupOutlet.alpha = valid ? 1.0 : 0.5}).disposed(by: disposeBag)viewModel.validatedUsername.bind(to: usernameValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.validatedPassword.bind(to: passwordValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.validatedPasswordRepeated.bind(to: repeatedPasswordValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.signingIn.bind(to: signingUpOulet.rx.isAnimating).disposed(by: disposeBag)viewModel.signedIn.subscribe(onNext: { signedIn inprint("User signed in \(signedIn)")}).disposed(by: disposeBag)//}let tapBackground = UITapGestureRecognizer()tapBackground.rx.event.subscribe(onNext: { [weak self] _ inself?.view.endEditing(true)}).disposed(by: disposeBag)view.addGestureRecognizer(tapBackground)}}
将用户行为传入给 ViewModel:
- username 将用户名输入框的当前文本传入
- password 将密码输入框的当前文本传入
- …
将 ViewModel 的输出状态显示出来:
- validatedUsername 用对应的
label将用户名验证结果显示出来 - validatedPassword 用对应的
label将密码验证结果显示出来 - …
整体结构
以下是全部的核心代码:
// ViewModelclass GithubSignupViewModel1 {// outputs {let validatedUsername: Observable<ValidationResult>let validatedPassword: Observable<ValidationResult>let validatedPasswordRepeated: Observable<ValidationResult>// Is signup button enabledlet signupEnabled: Observable<Bool>// Has user signed inlet signedIn: Observable<Bool>// Is signing process in progresslet signingIn: Observable<Bool>// }init(input: (username: Observable<String>,password: Observable<String>,repeatedPassword: Observable<String>,loginTaps: Observable<Void>),dependency: (API: GitHubAPI,validationService: GitHubValidationService,wireframe: Wireframe)) {let API = dependency.APIlet validationService = dependency.validationServicelet wireframe = dependency.wireframe/**Notice how no subscribe call is being made.Everything is just a definition.Pure transformation of input sequences to output sequences.*/validatedUsername = input.username.flatMapLatest { username inreturn validationService.validateUsername(username).observeOn(MainScheduler.instance).catchErrorJustReturn(.failed(message: "Error contacting server"))}.share(replay: 1)validatedPassword = input.password.map { password inreturn validationService.validatePassword(password)}.share(replay: 1)validatedPasswordRepeated = Observable.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword).share(replay: 1)let signingIn = ActivityIndicator()self.signingIn = signingIn.asObservable()let usernameAndPassword = Observable.combineLatest(input.username, input.password) { ($0, $1) }signedIn = input.loginTaps.withLatestFrom(usernameAndPassword).flatMapLatest { (username, password) inreturn API.signup(username, password: password).observeOn(MainScheduler.instance).catchErrorJustReturn(false).trackActivity(signingIn)}.flatMapLatest { loggedIn -> Observable<Bool> inlet message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"return wireframe.promptFor(message, cancelAction: "OK", actions: [])// propagate original value.map { _ inloggedIn}}.share(replay: 1)signupEnabled = Observable.combineLatest(validatedUsername,validatedPassword,validatedPasswordRepeated,signingIn.asObservable()) { username, password, repeatPassword, signingIn inusername.isValid &&password.isValid &&repeatPassword.isValid &&!signingIn}.distinctUntilChanged().share(replay: 1)}}// ViewControllerclass GitHubSignupViewController1 : ViewController {@IBOutlet weak var usernameOutlet: UITextField!@IBOutlet weak var usernameValidationOutlet: UILabel!@IBOutlet weak var passwordOutlet: UITextField!@IBOutlet weak var passwordValidationOutlet: UILabel!@IBOutlet weak var repeatedPasswordOutlet: UITextField!@IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!@IBOutlet weak var signupOutlet: UIButton!@IBOutlet weak var signingUpOulet: UIActivityIndicatorView!override func viewDidLoad() {super.viewDidLoad()let viewModel = GithubSignupViewModel1(input: (username: usernameOutlet.rx.text.orEmpty.asObservable(),password: passwordOutlet.rx.text.orEmpty.asObservable(),repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),loginTaps: signupOutlet.rx.tap.asObservable()),dependency: (API: GitHubDefaultAPI.sharedAPI,validationService: GitHubDefaultValidationService.sharedValidationService,wireframe: DefaultWireframe.shared))// bind results to {viewModel.signupEnabled.subscribe(onNext: { [weak self] valid inself?.signupOutlet.isEnabled = validself?.signupOutlet.alpha = valid ? 1.0 : 0.5}).disposed(by: disposeBag)viewModel.validatedUsername.bind(to: usernameValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.validatedPassword.bind(to: passwordValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.validatedPasswordRepeated.bind(to: repeatedPasswordValidationOutlet.rx.validationResult).disposed(by: disposeBag)viewModel.signingIn.bind(to: signingUpOulet.rx.isAnimating).disposed(by: disposeBag)viewModel.signedIn.subscribe(onNext: { signedIn inprint("User signed in \(signedIn)")}).disposed(by: disposeBag)//}let tapBackground = UITapGestureRecognizer()tapBackground.rx.event.subscribe(onNext: { [weak self] _ inself?.view.endEditing(true)}).disposed(by: disposeBag)view.addGestureRecognizer(tapBackground)}}
这里还有一个 Driver 版的演示代码,有兴趣的同学可以了解一下。
