Swift - RxSwift的使用详解70(RxFeedback架构2:一个用户注册样例)
这个用户注册样例我之前也做过,当时用的是 MVVM 架构(文章1、文章2)。下面我使用 RxFeedback 来对其进行重构。



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,主视图控制器(ViewController)
下面是本文的重点了。与 MVVM 架构不同的是,我们不再需要定义 ViewModel,而是通过状态、事件、反馈循环来实现整个页面的业务逻辑分离。import UIKit
import RxSwift
import RxCocoa
import RxFeedback
class ViewController: UIViewController {
//用户名输入框、以及验证结果显示标签
@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!
let disposeBag = DisposeBag()
//状态
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? //注册结果
//用户注册信息(只有开始注册状态下才有数据返回)
var signupData: (username: String, password: String)? {
return startSignup ? (username ?? "", password ?? "") : nil
}
//返回初始化状态
static var empty: State {
return State(username: nil, password: nil, repeatedPassword: nil,
usernameValidationResult: ValidationResult.empty,
passwordValidationResult: ValidationResult.empty,
repeatedPasswordValidationResult: ValidationResult.empty,
startSignup: false, signupResult: nil)
}
}
//事件
enum Event {
case usernameChanged(String) //用户名输入
case passwordChanged(String) //密码输入
case repeatedPasswordChanged(String) //重复密码输入
case usernameValidated(ValidationResult) //用户名验证结束
case signup //用户注册
case signupResponse(Bool) //注册响应
}
override func viewDidLoad() {
super.viewDidLoad()
//GitHub网络请求服务
let networkService = GitHubNetworkService()
//用户注册服务
let signupService = GitHubSignupService()
Driver.system(
//初始状态
initialState: State.empty,
//各个事件对状态的改变
reduce: { (state, event) -> State in
switch event {
case .usernameChanged(let value):
var result = state
result.username = value
result.signupResult = nil //防止弹出框重复弹出
return result
case .passwordChanged(let value):
var result = state
result.password = value
//验证密码
result.passwordValidationResult =
signupService.validatePassword(result.password ?? "")
//验证密码重复输入
if result.repeatedPassword != nil {
result.repeatedPasswordValidationResult =
signupService.validateRepeatedPassword(
result.password ?? "",
repeatedPassword: result.repeatedPassword ?? ""
)
}
result.signupResult = nil
return result
case .repeatedPasswordChanged(let value):
var result = state
result.repeatedPassword = value
//验证密码重复输入
result.repeatedPasswordValidationResult =
signupService.validateRepeatedPassword(
result.password ?? "",
repeatedPassword: result.repeatedPassword ?? ""
)
result.signupResult = nil
return result
case .usernameValidated(let value):
var result = state
result.usernameValidationResult = value
result.signupResult = nil
return result
case .signup:
var result = state
result.startSignup = true
result.signupResult = nil
return result
case .signupResponse(let value):
var result = state
result.startSignup = false
result.signupResult = value
return result
}
},
feedback:
//UI反馈
bind(self) { me, state in
//状态输出到页面控件上
let subscriptions = [
//用户名验证结果绑定
state.map{ $0.usernameValidationResult }
.drive(me.usernameValidationOutlet.rx.validationResult),
//密码验证结果绑定
state.map{ $0.passwordValidationResult }
.drive(me.passwordValidationOutlet.rx.validationResult),
//重复密码验证结果绑定
state.map{ $0.repeatedPasswordValidationResult }
.drive(me.repeatedPasswordValidationOutlet.rx.validationResult),
//注册按钮是否可用
state.map{ $0.usernameValidationResult.isValid &&
$0.passwordValidationResult.isValid &&
$0.repeatedPasswordValidationResult.isValid}
.drive(onNext: { valid in
me.signupOutlet.isEnabled = valid
me.signupOutlet.alpha = valid ? 1.0 : 0.3
}),
//活动指示器绑定
state.map{ $0.startSignup }
.drive(me.signInActivityIndicator.rx.isAnimating),
//注册结果显示
state.map{ $0.signupResult }
.filter{ $0 != nil }
.drive(onNext: { result in
me.showMessage("注册" + (result! ? "成功" : "失败") + "!")
})
]
//将 UI 事件变成Event输入到反馈循环里面去
let events = [
//用户名输入
me.usernameOutlet.rx.text.orEmpty.changed
.asSignal().map(Event.usernameChanged),
//密码输入
me.passwordOutlet.rx.text.orEmpty.changed
.asSignal().map(Event.passwordChanged),
//重复密码输入
me.repeatedPasswordOutlet.rx.text.orEmpty.changed
.asSignal().map(Event.repeatedPasswordChanged),
//注册按钮点击
me.signupOutlet.rx.tap
.asSignal().map{ _ in Event.signup },
]
return Bindings(subscriptions: subscriptions, events: events)
},
//非UI的自动反馈(用户名验证)
react(query: { $0.username }, effects: { username in
return signupService.validateUsername(username)
.asSignal(onErrorRecover: { _ in .empty() })
.map(Event.usernameValidated)
}),
//非UI的自动反馈(用户注册)
react(query: { $0.signupData }, effects: { (username, password) in
return networkService.signup(username, password: password)
.asSignal(onErrorRecover: { _ in .empty() })
.map(Event.signupResponse)
})
)
.drive()
.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)
}
}
附:代码的拆分优化
上面样例中我们把 RxFeedBack 架构用到的 state、event、feedback 等等都定义在 ViewController 中,这样功能一旦多些,ViewController 里的代码就会变得十分冗长,难以阅读。下面我对代码进行改成,拆分成各个独立的文件。
1,GitHubSignup+State.swift
将 State、Event 以及相关的 reduce 都放在这里面。
import Foundation
//状态
struct GitHubSignupState {
var username: String? //用户名
var password: String? //密码
var repeatedPassword: String? //再出输入密码
var usernameValidationResult: ValidationResult //用户名验证结果
var passwordValidationResult: ValidationResult //密码验证结果
var repeatedPasswordValidationResult: ValidationResult //重复密码验证结果
var startSignup: Bool //开始注册
var signupResult: Bool? //注册结果
//用户注册信息(只有开始注册状态下才有数据返回)
var signupData: (username: String, password: String)? {
return startSignup ? (username ?? "", password ?? "") : nil
}
}
//事件
enum GitHubSignupEvent {
case usernameChanged(String) //用户名输入
case passwordChanged(String) //密码输入
case repeatedPasswordChanged(String) //重复密码输入
case usernameValidated(ValidationResult) //用户名验证结束
case signup //用户注册
case signupResponse(Bool) //注册响应
}
extension GitHubSignupState {
//返回初始化状态
static var empty: GitHubSignupState {
return GitHubSignupState(username: nil, password: nil, repeatedPassword: nil,
usernameValidationResult: ValidationResult.empty,
passwordValidationResult: ValidationResult.empty,
repeatedPasswordValidationResult: ValidationResult.empty,
startSignup: false, signupResult: nil)
}
static func reduce(state: GitHubSignupState, event: GitHubSignupEvent
, signupService: GitHubSignupService) -> GitHubSignupState {
switch event {
case .usernameChanged(let value):
var result = state
result.username = value
result.signupResult = nil //防止弹出框重复弹出
return result
case .passwordChanged(let value):
var result = state
result.password = value
//验证密码
result.passwordValidationResult =
signupService.validatePassword(result.password ?? "")
//验证密码重复输入
if result.repeatedPassword != nil {
result.repeatedPasswordValidationResult =
signupService.validateRepeatedPassword(
result.password ?? "",
repeatedPassword: result.repeatedPassword ?? ""
)
}
result.signupResult = nil
return result
case .repeatedPasswordChanged(let value):
var result = state
result.repeatedPassword = value
//验证密码重复输入
result.repeatedPasswordValidationResult =
signupService.validateRepeatedPassword(
result.password ?? "",
repeatedPassword: result.repeatedPassword ?? ""
)
result.signupResult = nil
return result
case .usernameValidated(let value):
var result = state
result.usernameValidationResult = value
result.signupResult = nil
return result
case .signup:
var result = state
result.startSignup = true
result.signupResult = nil
return result
case .signupResponse(let value):
var result = state
result.startSignup = false
result.signupResult = value
return result
}
}
}
2,GitHubSignup+Feedback.swift
这里面放置与 UI 无关的反馈。
import Foundation
import RxSwift
import RxCocoa
import RxFeedback
struct GitHubSignupFeedback {
//验证用户名
static func validateUsername(signupService: GitHubSignupService)
-> (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> {
let query:(GitHubSignupState) -> String?
= { $0.username }
let effects:(String) -> Signal<GitHubSignupEvent>
= { return signupService.validateUsername($0)
.asSignal(onErrorRecover: { _ in .empty() })
.map(GitHubSignupEvent.usernameValidated) }
return react(query: query, effects: effects)
}
//用户注册
static func signup(networkService: GitHubNetworkService)
-> (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> {
let query:(GitHubSignupState) -> (String, String)?
= { $0.signupData }
let effects:(String, String) -> Signal<GitHubSignupEvent>
= { return networkService.signup($0, password: $1)
.asSignal(onErrorRecover: { _ in .empty() })
.map(GitHubSignupEvent.signupResponse) }
return react(query: query, effects: effects)
}
}
3,ViewController.swift
可以发现代码经过前面的提取之后,主视图控制器的代码变得简洁许多。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback
class ViewController: UIViewController {
//用户名输入框、以及验证结果显示标签
@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!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
//GitHub网络请求服务
let networkService = GitHubNetworkService()
//用户注册服务
let signupService = GitHubSignupService()
//UI绑定
let bindUI: (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> =
bind(self) { me, state in
//状态输出到页面控件上
let subscriptions = [
//用户名验证结果绑定
state.map{ $0.usernameValidationResult }
.drive(me.usernameValidationOutlet.rx.validationResult),
//密码验证结果绑定
state.map{ $0.passwordValidationResult }
.drive(me.passwordValidationOutlet.rx.validationResult),
//重复密码验证结果绑定
state.map{ $0.repeatedPasswordValidationResult }
.drive(me.repeatedPasswordValidationOutlet.rx.validationResult),
//注册按钮是否可用
state.map{ $0.usernameValidationResult.isValid &&
$0.passwordValidationResult.isValid &&
$0.repeatedPasswordValidationResult.isValid}
.drive(onNext: { valid in
me.signupOutlet.isEnabled = valid
me.signupOutlet.alpha = valid ? 1.0 : 0.3
}),
//活动指示器绑定
state.map{ $0.startSignup }
.drive(me.signInActivityIndicator.rx.isAnimating),
//注册结果显示
state.map{ $0.signupResult }
.filter{ $0 != nil }
.drive(onNext: { result in
me.showMessage("注册" + (result! ? "成功" : "失败") + "!")
})
]
//将 UI 事件变成Event输入到反馈循环里面去
let events = [
//用户名输入
me.usernameOutlet.rx.text.orEmpty.changed
.asSignal().map(GitHubSignupEvent.usernameChanged),
//密码输入
me.passwordOutlet.rx.text.orEmpty.changed
.asSignal().map(GitHubSignupEvent.passwordChanged),
//重复密码输入
me.repeatedPasswordOutlet.rx.text.orEmpty.changed
.asSignal().map(GitHubSignupEvent.repeatedPasswordChanged),
//注册按钮点击
me.signupOutlet.rx.tap
.asSignal().map{ _ in GitHubSignupEvent.signup },
]
return Bindings(subscriptions: subscriptions, events: events)
}
Driver.system(
//初始状态
initialState: GitHubSignupState.empty,
//各个事件对状态的改变
reduce: { GitHubSignupState.reduce(state: $0, event: $1,
signupService: signupService) },
feedback:
//UI反馈
bindUI,
//非UI的自动反馈(用户名验证)
GitHubSignupFeedback.validateUsername(signupService: signupService),
//非UI的自动反馈(用户注册)
GitHubSignupFeedback.signup(networkService: networkService)
)
.drive()
.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)
}
}
