Swift - RxSwift的使用详解73(ReactorKit架构2:一个用户注册样例)
作者:hangge | 2018-06-29 08:10
这个用户注册样例我之前也做过,当时分别用的是 MVVM 架构(文章1、文章2)以及 RxFeedback 架构(点击查看)。下面我使用 ReactorKit 来对其进行重构。
1,效果图
(1)默认“注册”按钮不可用,只有用户名、密码、再次输入密码三者都符合如下条件时才可用:
- 输入用户名时会同步检查该用户名是否符合条件(只能为数字或字母),以及是否已存在(网络请求),并在输入框下方显示验证结果。(当连续输入时,如果上一次请求还会返未会自动取消,只处理最新请求。)
- 输入密码时会检查密码是否符合条件(最少要 5 位),并在输入框下方显示验证结果。
- 再次输入密码时会检查两个密码是否一致,并在输入框下方显示验证结果。
(2)当所有输入都符合条件时,点击“注册”按钮发起请求,并将结果弹出显示。同时在注册过程中按钮左侧会显示一个菊花状的网络请求指示器。
2,页面设计
(1)首先我们在 storyboard 中添加 3 个输入框、3 个文本标签,它们分别用于输入用户名、密码、确认密码,以及对应的验证结果显示。
(2)接着在界面最下方添加一个按钮用于注册。
(3)接着在注册按钮的左侧放置一个 Activity Indicator View,同时设置当其动画停止时自动隐藏。
(4)最后将这个 8 个 UI 控件与代码做 @IBOutlet 关联。
3,网络请求服务
我们首先将需要调用的网络请求:验证用户名是否存在,用户注册封装起来(GitHubNetworkService.swift),方便后面使用。(这里代码与之前那篇文章一样,没有变化)
import Foundation import RxSwift //GitHub网络请求服务 class GitHubNetworkService { //验证用户是否存在 func usernameAvailable(_ username: String) -> Observable<Bool> { //通过检查这个用户的GitHub主页是否存在来判断用户是否存在 let url = URL(string: "https://github.com/\(username.URLEscaped)")! let request = URLRequest(url: url) return URLSession.shared.rx.response(request: request) .map { pair in //如果不存在该用户主页,则说明这个用户名可用 return pair.response.statusCode == 404 } .catchErrorJustReturn(false) } //注册用户 func signup(_ username: String, password: String) -> Observable<Bool> { //这里我们没有真正去发起请求,而是模拟这个操作(平均每3次有1次失败) let signupResult = arc4random() % 3 == 0 ? false : true return Observable.just(signupResult) .delay(1.5, scheduler: MainScheduler.instance) //结果延迟1.5秒返回 } } //扩展String extension String { //字符串的url地址转义 var URLEscaped: String { return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" } }
4,用户注册验证服务
(1)首先定义一个用于表示验证结果和信息的枚举(ValidationResult),后面我们会将它作为验证结果绑定到界面上。(这里代码与之前那篇文章一样,没有变化)
(2)接着将用户名、密码等各种需要用到的验证封装起来(GitHubSignupService.swift),方便后面使用。(返回的就是上面定义的 ValidationResult,这里代码与之前那篇文章一样,没有变化)
import UIKit //验证结果和信息的枚举 enum ValidationResult { case validating //正在验证中s case empty //输入为空 case ok(message: String) //验证通过 case failed(message: String) //验证失败 } //扩展ValidationResult,对应不同的验证结果返回验证是成功还是失败 extension ValidationResult { var isValid: Bool { switch self { case .ok: return true default: return false } } } //扩展ValidationResult,对应不同的验证结果返回不同的文字描述 extension ValidationResult: CustomStringConvertible { var description: String { switch self { case .validating: return "正在验证..." case .empty: return "" case let .ok(message): return message case let .failed(message): return message } } } //扩展ValidationResult,对应不同的验证结果返回不同的文字颜色 extension ValidationResult { var textColor: UIColor { switch self { case .validating: return UIColor.gray case .empty: return UIColor.black case .ok: return UIColor(red: 0/255, green: 130/255, blue: 0/255, alpha: 1) case .failed: return UIColor.red } } }
(2)接着将用户名、密码等各种需要用到的验证封装起来(GitHubSignupService.swift),方便后面使用。(返回的就是上面定义的 ValidationResult,这里代码与之前那篇文章一样,没有变化)
import UIKit import RxSwift //用户注册服务 class GitHubSignupService { //密码最少位数 let minPasswordCount = 5 //网络请求服务 lazy var networkService = { return GitHubNetworkService() }() //验证用户名 func validateUsername(_ username: String) -> Observable<ValidationResult> { //判断用户名是否为空 if username.isEmpty { return .just(.empty) } //判断用户名是否只有数字和字母 if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil { return .just(.failed(message: "用户名只能包含数字和字母")) } //发起网络请求检查用户名是否已存在 return networkService .usernameAvailable(username) .map { available in //根据查询情况返回不同的验证结果 if available { return .ok(message: "用户名可用") } else { return .failed(message: "用户名已存在") } } .startWith(.validating) //在发起网络请求前,先返回一个“正在检查”的验证结果 } //验证密码 func validatePassword(_ password: String) -> ValidationResult { let numberOfCharacters = password.count //判断密码是否为空 if numberOfCharacters == 0 { return .empty } //判断密码位数 if numberOfCharacters < minPasswordCount { return .failed(message: "密码至少需要 \(minPasswordCount) 个字符") } //返回验证成功的结果 return .ok(message: "密码有效") } //验证二次输入的密码 func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult { //判断密码是否为空 if repeatedPassword.count == 0 { return .empty } //判断两次输入的密码是否一致 if repeatedPassword == password { return .ok(message: "密码有效") } else { return .failed(message: "两次输入的密码不一致") } } }
5,对 UILabel 进行扩展(BindingExtensions.swift)
为了让 ValidationResult 能绑定到 label 上,我们要对 UILabel 进行扩展(这里代码与之前那篇文章一样,还是没有变化)import UIKit import RxSwift import RxCocoa //扩展UILabel extension Reactive where Base: UILabel { //让验证结果(ValidationResult类型)可以绑定到label上 var validationResult: Binder<ValidationResult> { return Binder(base) { label, result in label.textColor = result.textColor label.text = result.description } } }
6,反应器(ViewReactor.swift)
这个就是本文的重点了,在这里我们将 Action 转换成 State。import RxSwift final class ViewReactor: Reactor { //GitHub网络请求服务 let networkService = GitHubNetworkService() //用户注册服务 let signupService = GitHubSignupService() //代表用户行为 enum Action { case usernameChanged(String) //用户名输入 case passwordChanged(String) //密码输入 case repeatedPasswordChanged(String) //重复密码输入 case signup //用户注册 } //代表状态变化 enum Mutation { case setUsername(String) //设置用户名 case setUsernameValidationResult(ValidationResult) //设置用户名验证结果 case setPassword(String) //设置密码 case setPasswordValidationResult(ValidationResult) //设置用户名验证结果 case setRepeatedPassword(String) //设置重复密码 case setRepeatedPasswordValidationResult(ValidationResult) //设置重复密码验证结果 case setStartSignup(Bool) //设置注册状态(是否正在提交注册) case setSignupResult(Bool?) //设置注册结果 } //代表页面状态 struct State { var username: String //用户名 var password: String //密码 var repeatedPassword: String //再出输入密码 var usernameValidationResult: ValidationResult //用户名验证结果 var passwordValidationResult: ValidationResult //密码验证结果 var repeatedPasswordValidationResult: ValidationResult //重复密码验证结果 var startSignup: Bool //开始注册 var signupResult: Bool? //注册结果 } //初始页面状态 let initialState: State init() { self.initialState = State( username: "", password: "", repeatedPassword: "", usernameValidationResult: ValidationResult.empty, passwordValidationResult: ValidationResult.empty, repeatedPasswordValidationResult: ValidationResult.empty, startSignup: false, signupResult: nil ) } //实现 Action -> Mutation 的转换 func mutate(action: Action) -> Observable<Mutation> { switch action { //用户名变化 case let .usernameChanged(username): //依次执行下面2个状态变化动作 return Observable.concat([ //设置用户名 Observable.just(Mutation.setUsername(username)), //设置用户名验证结果 signupService.validateUsername(username) //如果用户名再次变化,但上一次验证还未完成,上一次验证会自动取消 .takeUntil(self.action.filter(isUsernameChangedAction)) .map(Mutation.setUsernameValidationResult) ]) //密码变化 case let .passwordChanged(password): //依次执行下面2个状态变化动作 return Observable.concat([ //设置密码 Observable.just(Mutation.setPassword(password)), //设置密码验证结果 Observable.just( Mutation.setPasswordValidationResult( signupService.validatePassword(password)) ), //设置重复密码验证结果 Observable.just( Mutation.setRepeatedPasswordValidationResult( signupService.validateRepeatedPassword( password, repeatedPassword: self.currentState.repeatedPassword ) ) ) ]) //重复密码变化 case let .repeatedPasswordChanged(repeatedPassword): //依次执行下面2个状态变化动作 return Observable.concat([ //设置重复密码 Observable.just(Mutation.setRepeatedPassword(repeatedPassword)), //设置重复密码验证结果 Observable.just( Mutation.setRepeatedPasswordValidationResult( signupService.validateRepeatedPassword( self.currentState.password, repeatedPassword: repeatedPassword ) ) ) ]) //注册按钮点击 case .signup: //依次执行下面4个状态变化动作 return Observable.concat([ //先清空之前的结果 Observable.just(Mutation.setSignupResult(nil)), //开始注册 Observable.just(Mutation.setStartSignup(true)), //设置注册结果 networkService.signup(self.currentState.username, password: self.currentState.password) .map(Mutation.setSignupResult), //注册结束 Observable.just(Mutation.setStartSignup(false)) ]) } } //实现 Mutation -> State 的转换 func reduce(state: State, mutation: Mutation) -> State { //从旧状态那里复制一个新状态 var state = state //根据状态变化设置响应的状态值 switch mutation { //设置用户名 case let .setUsername(username): state.username = username state.signupResult = nil //信息一有变化则清除注册结果 //设置用户名验证结果 case let .setUsernameValidationResult(validationResult): state.usernameValidationResult = validationResult //设置密码 case let .setPassword(password): state.password = password state.signupResult = nil //信息一有变化则清除注册结果 //设置密码验证结果 case let .setPasswordValidationResult(validationResult): state.passwordValidationResult = validationResult //设置重复密码 case let .setRepeatedPassword(repeatedPassword): state.repeatedPassword = repeatedPassword state.signupResult = nil //信息一有变化则清除注册结果 //设置重复密码验证结果 case let .setRepeatedPasswordValidationResult(validationResult): state.repeatedPasswordValidationResult = validationResult //设置注册状态 case let .setStartSignup(value): state.startSignup = value //设置注册结果 case let .setSignupResult(value): state.signupResult = value } //返回新状态 return state } //判断当前的 action 是否是用户名输入 private func isUsernameChangedAction(_ action: Action) -> Bool { if case .usernameChanged = action { return true } else { return false } } }
7,视图(ViewController.swift)
主视图控制器里的代码就很简单了,只需要将页面上的用户行为绑定到 reactor 里的 action 上,同时将 reactor 里的 state 绑定到页面 UI 控件上即可。import UIKit import RxSwift import RxCocoa class ViewController: UIViewController, StoryboardView { //用户名输入框、以及验证结果显示标签 @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 signInActivityIndicator: UIActivityIndicatorView! var disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //设置reactor,会自动触发bind()方法 self.reactor = ViewReactor() } //处理绑定事件(该方法会在 self.reactor 变化时自动触发) func bind(reactor: ViewReactor) { //Action(实现 View -> Reactor 的绑定) usernameOutlet.rx.text.orEmpty.changed //用户名输入框文字改变事件 .throttle(0.3, scheduler: MainScheduler.instance) .distinctUntilChanged() .map(Reactor.Action.usernameChanged) //转换为 Action.usernameChanged .bind(to: reactor.action) //绑定到 reactor.action .disposed(by: disposeBag) passwordOutlet.rx.text.orEmpty.changed //密码输入框文字改变事件 .distinctUntilChanged() .map(Reactor.Action.passwordChanged) //转换为 passwordChanged .bind(to: reactor.action) //绑定到 reactor.action .disposed(by: disposeBag) repeatedPasswordOutlet.rx.text.orEmpty.changed //重复密码输入框文字改变事件 .distinctUntilChanged() .map(Reactor.Action.repeatedPasswordChanged) //转换为 repeatedPasswordChanged .bind(to: reactor.action) //绑定到 reactor.action .disposed(by: disposeBag) signupOutlet.rx.tap //注册按钮点击事件 .map{ Reactor.Action.signup } //转换为 signup .bind(to: reactor.action) //绑定到 reactor.action .disposed(by: disposeBag) // State(实现 Reactor -> View 的绑定) reactor.state.map { $0.usernameValidationResult } //得到最新用户名验证结果 .bind(to: usernameValidationOutlet.rx.validationResult) //绑定到文本标签上 .disposed(by: disposeBag) reactor.state.map { $0.passwordValidationResult } //得到最新密码验证结果 .bind(to: passwordValidationOutlet.rx.validationResult) //绑定到文本标签上 .disposed(by: disposeBag) reactor.state.map { $0.repeatedPasswordValidationResult } //得到最新重复密码验证结果 .bind(to: repeatedPasswordValidationOutlet.rx.validationResult)//绑定到文本标签上 .disposed(by: disposeBag) //注册按钮是否可用 reactor.state.map{ $0.usernameValidationResult.isValid && $0.passwordValidationResult.isValid && $0.repeatedPasswordValidationResult.isValid } .subscribe(onNext: { [weak self] valid in self?.signupOutlet.isEnabled = valid self?.signupOutlet.alpha = valid ? 1.0 : 0.3 }) .disposed(by: disposeBag) //活动指示器绑定 reactor.state.map { $0.startSignup } .bind(to: signInActivityIndicator.rx.isAnimating) .disposed(by: disposeBag) //注册结果显示 reactor.state.map { $0.signupResult } .filter{ $0 != nil } .subscribe(onNext: { [weak self] result in self?.showMessage("注册" + (result! ? "成功" : "失败") + "!") }) .disposed(by: disposeBag) } //详细提示框 func showMessage(_ message: String) { let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil) alertController.addAction(okAction) self.present(alertController, animated: true, completion: nil) } }
全部评论(0)