搭建UI
如下图所示,搭建好UI并连线:
弹框
由于弹框属于View层,但是又得在ViewModel中使用,这违背了MVVM模式中ViewModel不能引用View的限制。所以通过协议来解决这个问题,在ViewModel层中定义如下协议:
protocol WireFrame {
/// 弹框
/// - Parameters:
/// - title: 标题
/// - message: 信息
/// - cancelAction: 取消按钮
/// - actions: 其他按钮数组
/// - animated: 是否带动画
/// - completion: 完成闭包
func promptFor<Action: CustomStringConvertible>(_ title: String, message: String, cancelAction: Action, actions: [Action]?, animated: Bool, completion: (() -> Void)?) -> Observable<Action>
}
在View层中定义一个DefaultWireFrame类实现上面的协议:
class DefaultWireFrame: WireFrame {
func promptFor<Action: CustomStringConvertible>(_ title: String, message: String, cancelAction: Action, actions: [Action]? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> Observable<Action> {
return Observable.create({ (observer) -> Disposable in
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: cancelAction.description, style: .cancel, handler: { (_) in
observer.onNext(cancelAction)
}))
if let actions = actions {
for action in actions {
alert.addAction(UIAlertAction(title: action.description, style: .default, handler: { (_) in
observer.onNext(action)
}))
}
}
DefaultWireFrame.rootViewController().present(alert, animated: animated, completion: completion)
return Disposables.create { [weak alert] in
alert?.dismiss(animated: animated, completion: nil)
}
})
}
}
代码分析:
- 使用create操作符创建Observable序列
- 创建UIAlertController并添加UIAlertAction,在所有UIAlertAction的处理事件中用
onNext
发出元素 - 使用
present
弹出弹框,构建并返回资源清理对象Disposable
,清理资源时使用dismiss
关闭弹框
Model层
model层表示程序的状态——业务逻辑
业务逻辑
在Model层中创建ValidationResult结构用以表示用户数据是否有效:
/// 有效结果
enum ValidationResult {
case ok(message: String)// 有效
case empty // 空
case validating // 验证中
case failed(message: String) // 失败
}
在Model层为Sting类型扩展扩展一个URLEscaped属性用以提供URL编码的String:
extension String {
var URLEscaped: String {
return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
}
}
Model层暴露服务给ViewModel,在Model层定义如下协议,暴露API接口:
/// Github网络服务接口
protocol GithubApi {
/// 用户名是否有效
/// - Parameter username: 用户名
func usernameAvailable(_ username: String) -> Observable<Bool>
/// 注册
/// - Parameters:
/// - username: 用户名
/// - password: 密码
func signup(_ username: String, password: String) -> Observable<Bool>
}
/// GitHub数据是否有效接口
protocol GitHubValidationService {
/// 判断用户名是否有效
/// - Parameter username: 用户名
func validateUsername(_ username: String) -> Observable<ValidationResult>
/// 判断密码是否有效
/// - Parameter password: 密码
func validatePassword(_ password: String) -> ValidationResult
/// 判断二次输入的密码是否有效
/// - Parameters:
/// - password: 第一次输入的密码
/// - repeatPassword: 第二次输入的密码
func validateRepeatedPassword(_ password: String, repeatPassword: String) -> ValidationResult
}
在Model层定义GitHubDefaultAPI
类遵守GithubApi
协议所暴露的接口:
class GitHubDefaultAPI: GithubApi {
let session: URLSession
init(_ session: URLSession) {
self.session = session
}
static let shareApi = GitHubDefaultAPI(URLSession.shared)
/// 检验用户名是否有效
/// - Parameter username: 用户名
func usernameAvailable(_ username: String) -> Observable<Bool> {
let url = URL(string: "https://github.com/\(username.URLEscaped)")!
let request = URLRequest(url: url)
// 直接获取github用户数据
return session.rx.response(request: request).map({ (pair) -> Bool in
// 如果404错误则用户名有效,说明没有被使用
return pair.response.statusCode == 404
}).catchErrorJustReturn(false)
}
/// 注册
/// - Parameters:
/// - username: 用户名
/// - password: 密码
func signup(_ username: String, password: String) -> Observable<Bool> {
// 四分之一的几率失败
let result = arc4random()%4 == 0 ? false : true
// 模拟网络请求
return Observable.just(result).delay(.milliseconds(1000 + Int(arc4random()%3000)), scheduler: MainScheduler.instance)
}
}
代码分析:
usernameAvailable
序列使用URLSession请求github的对应用户,如果成功证明用户名已经存在。使用catchErrorJustReturn
操作符捕捉网络错误signup
序列是模拟的网络请求,通过just
操作符构建一个随机元素,delay
操作符模拟一段随机请求时间
在Model层定义GitHubDefaultValidationService
类遵守GitHubValidationService
协议所暴露的接口:
/// 服务
class GitHubDefaultValidationService: GitHubValidationService {
/// 接口
let api: GithubApi
/// 密码最少位数
let minPasswordCount = 6
/// 密码最大位数
let maxPasswordCount = 24
init(_ api: GithubApi) {
self.api = api
}
func validateUsername(_ username: String) -> Observable<ValidationResult> {
if username.isEmpty {
return .just(.empty)
}
let loadingValue = ValidationResult.validating
return api.usernameAvailable(username).map({ (vailable) -> ValidationResult in
if vailable {
return .ok(message: "用户名有效")
} else {
return .failed(message: "用户名无效")
}
}).startWith(loadingValue)
}
func validatePassword(_ password: String) -> ValidationResult {
if password.isEmpty {
return .empty
}
if password.count < minPasswordCount {
return .failed(message: "密码不能小于\(minPasswordCount)6位")
} else if password.count > maxPasswordCount {
return .failed(message: "密码不能大于于\(maxPasswordCount)位")
}
return .ok(message: "密码可用")
}
func validateRepeatedPassword(_ password: String, repeatPassword: String) -> ValidationResult {
if repeatPassword.isEmpty {
return .empty
}
if repeatPassword == password {
return .ok(message: "密码相同")
} else {
return .failed(message: "密码不同")
}
}
}
代码分析:
- 使用常量minPasswordCount、maxPasswordCount定义好密码的最小和最大长度
- validateUsername序列,用户名为空时返回空序列,否则请求网络将结果使用map操作符转化为
ValidationResult
类型,并使用startWith
操作符设置一个初始元素 - validatePassword序列跟validateUsername序列类似,主要差别就是密码是根据长度来判断有效性的
- validateRepeatedPassword序列是通过两次密码是否相同来判断有效性的
活动指示器
首先定义一个ActivityToken:
/// 活动令牌
struct ActivityToken<E>: ObservableConvertibleType, Disposable {
let _source: Observable<E>
let _dispose: Cancelable
init(source: Observable<E>, disposeAction: @escaping () -> Void) {
_source = source
_dispose = Disposables.create(with: disposeAction)
}
func asObservable() -> Observable<E> {
return _source
}
func dispose() {
_dispose.dispose()
}
}
ActivityToken遵守Disposable、ObservableConvertibleType协议,也就是说ActivityToken既是Disposable也是Observable。实际上是用来存储一个Observable和Disposable清理资源的闭包。
- 作为Observable时本质上是初始化传入的序列
- 作为Disposable时本质上是初始化传入的闭包创建的Disposable
定义ActivityIndicator:
/// 活动指示器
class ActivityIndicator: SharedSequenceConvertibleType {
typealias Element = Bool
typealias SharingStrategy = DriverSharingStrategy
/// 锁
let lock = NSRecursiveLock()
/// 计数序列
let relay = BehaviorRelay(value: 0)
/// 加载序列
let loading: SharedSequence<SharingStrategy, Bool>
init() {
loading = relay.asDriver().map({ $0 > 0 }).distinctUntilChanged()
}
/// 增量计数
func increment() {
lock.lock()
relay.accept(relay.value + 1)
lock.unlock()
}
/// 减量计数
func decrement() {
lock.lock()
relay.accept(relay.value - 1)
lock.unlock()
}
/// 跟踪活动
/// - Parameter source: 源序列
func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {
return Observable.using({ [weak self] () -> ActivityToken<Source.Element> in
// 增量计数
self?.increment()
// 返回一个Disposable
return ActivityToken(source: source.asObservable(), disposeAction: self?.decrement ?? {})
}, observableFactory: { (t) in
// 返回一个序列
t.asObservable()
})
}
/// 遵守协议
func asSharedSequence() -> SharedSequence<DriverSharingStrategy, Bool> {
return loading
}
}
ActivityIndicator的实现思想类似于内存管理中的引用计数,通过increment/decrement这两个函数来增/减计数的值,再使用BehaviorRelay把这些计数值作为元素发送出来,最后通过map操作符将元素转换为转化为BOOL类型的序列。
trackActivityOfObservable函数实现:
- 该函数接收一个Observable序列作为参数
- 执行增量计数函数
- 把减量计数函数和参数序列包装到ActivityToken中
- 使用using操作符,把前面的ActivityToken作为resourceFactory(序列完成需要清理的资源)参数。保证参数序列完成时,清理资源的同时执行减量计数函数
- 返回结果序列
扩展ObservableConvertibleType协议,方便Observable序列记录活动状态:
extension ObservableConvertibleType {
func trackActivity(_ indicator: ActivityIndicator) -> Observable<Element> {
return indicator.trackActivityOfObservable(self)
}
}
使用Observeble的实现方案
MVVM模式的核心是ViewModel,它是一种特殊的model类型,用于表示程序的UI状态,包含描述每个UI控件的状态,所有的UI逻辑都在ViewModel中。
实际上ViewModel暴露属性来表示UI状态,它同样暴露命令来表示UI操作(通常是方法)。ViewModel负责管理基于用户交互的UI状态的改变。
在ViewModel层创建SignupObservableVM,并定义如下序列来表示各种UI状态:
class SignupObservableVM {
// 用户名有效验证的序列
let validatedUsername: Observable<ValidationResult>
// 密码有效验证的序列
let validatedPassword: Observable<ValidationResult>
// 重复密码有效验证的序列
let validatedRepeatedPassword: Observable<ValidationResult>
// 允许注册的序列
let signupEnabled: Observable<Bool>
// 注册的序列
let signedIn: Observable<Bool>
// 注册中的序列
let signingIn: Observable<Bool>
初始化ViewModel,接收View层的事件:
/// 初始化
/// - Parameters:
/// - input: 输入序列元组
/// - dependency: 依赖的功能模型
init(
input: (
username: Observable<String>,// 用户名输入序列
password: Observable<String>,// 密码输入序列
repeatedPassword: Observable<String>,// 二次密码输入序列
signTaps: Observable<Void>),// 注册点击序列
dependency: (
API: GithubApi,
service: GitHubValidationService,
wireframe: WireFrame))
有效验证状态
在初始化函数中实现表示用户名、密码、二次输入密码是否有效的序列:
validatedUsername = input.username
.flatMapLatest({ (name) in
return service.validateUsername(name)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(.failed(message: "服务器报错"))
}).share(replay: 1)
validatedPassword = input.password
.map({ (password) in
return service.validatePassword(password)
}).share(replay: 1)
validatedRepeatedPassword = Observable
.combineLatest(input.password, input.repeatedPassword, resultSelector: service.validateRepeatedPassword)
.share(replay: 1)
说明:使用map操作符做元素的转换,由于validatedUsername返回的一个序列所以使用flatMapLatest操作符来展平及忽略旧的序列,用observeOn操作符来保证线程,用catchErrorJustReturn操作符保证不会发生错误。最后使用share操作符来共享序列元素。
注册状态
创建一个ActivityIndicator序列实例用于表示是否正在注册中:
let signingIn = ActivityIndicator()
self.signingIn = signingIn.asObservable()
组合用户名和密码的输入序列:
let up = Observable.combineLatest(input.username, input.password) { (username: $0, password: $1) }
实现表示是否允许注册的signupEnabled序列:
signupEnabled = Observable
.combineLatest(
validatedUsername,
validatedPassword,
validatedRepeatedPassword,
self.signingIn,
resultSelector: { (un, pd, repd, sign) in
un.isValidate && pd.isValidate && repd.isValidate && !sign
}
).distinctUntilChanged()
.share(replay: 1)
代码分析:
- 使用combineLatest操作符合并用户名有效验证、密码有效验证、二次输入密码有效验证、是否正在注册中的序列,组合
不在登录中,其他全部有效
的BOOL值返回 - 使用distinctUntilChanged操作符保证序列值发生变化时发出元素
- 最后使用share操作符达到共享序列元素的效果
实现表示是否注册成功的signedIn序列:
signedIn = input.signTaps
.withLatestFrom(up)
.flatMapLatest({ (pair) in
return api.signup(pair.username, password: pair.password)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
.trackActivity(signingIn)
}).flatMapLatest({ (loggedIn) -> Observable<Bool> in
let message = loggedIn ? "GitHub注册成功" : "GitHub注册失败"
return DefaultWireFrame()
.promptFor("提示", message: message, cancelAction: "确定", actions: ["否"])
.map({ _ in loggedIn })
}).share(replay: 1)
代码分析:
- 注册点击序列使用withLatestFrom操作符将元素转化为用户名与密码的输入组合序列的最新元素
- 使用flatMapLatest操作符返回注册序列
- 注册序列使用observeOn、catchErrorJustReturn操作符保证注册序列在主线程执行并不会出错
- 注册序列执行trackActivity操作,记录序列的状态
- 再使用flatMapLatest操作符返回消息提示序列
- 最后使用share操作符达到共享序列元素的效果
绑定UI
定义一个ValidationColors结构体,使用3个类属性来表示个状态的颜色。:
/// 有效的颜色
struct ValidationColors {
static let defaultColor = UIColor.black // 默认黑色
static let okColor = UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0) // 有效为绿色
static let errorColor = UIColor.red // 失败为红色
}
扩展ValidationResult,定义isValidate、description、textColor3个计算属性:
/// 有效结果扩展
extension ValidationResult {
/// 是否有效
var isValidate: Bool {
switch self {
case .ok:
return true
default:
return false
}
}
/// 描述
var description: String {
switch self {
case let .ok(message):
return message
case .empty:
return ""
case .validating:
return "加载中..."
case let .failed(message):
return message
}
}
/// 文本颜色
var textColor: UIColor {
switch self {
case .ok:
return ValidationColors.okColor
case .empty:
return ValidationColors.defaultColor
case .validating:
return ValidationColors.defaultColor
case .failed:
return ValidationColors.errorColor
}
}
}
扩展泛型Base为UILabel类型的Reactive,增加一个Binder类型的validationResult属性将ValidationResult绑定到UILabel的textColor、text两个属性上:
extension Reactive where Base: UILabel {
var validationResult: Binder<ValidationResult> {
return Binder(base, binding: { (label, result) in
label.textColor = result.textColor
label.text = result.description
})
}
}
将ViewModel中各种表示UI状态的序列绑定到对应的UI控件上:
// 绑定UI
vm.validatedUsername
.bind(to: usernameValidationOutlet.rx.validationResult)
.disposed(by: bag)
vm.validatedPassword
.bind(to: passwordValidationOutlet.rx.validationResult)
.disposed(by: bag)
vm.validatedRepeatedPassword
.bind(to: repeatValidationOutlet.rx.validationResult)
.disposed(by: bag)
vm.signupEnabled
.bind(to: signupOutlet.rx.isEnabled)
.disposed(by: bag)
vm.signupEnabled
.map { $0 ? 1.0 : 0.5 }
.bind(to: signupOutlet.rx.alpha)
.disposed(by: bag)
vm.signingIn
.bind(to: signingupOutlet.rx.isAnimating)
.disposed(by: bag)
vm.signingIn
.subscribe(onNext: { [weak self] (signing) in
if (signing) { self?.view.endEditing(signing) }
}).disposed(by: bag)
vm.signedIn
.subscribe(onNext: { (signed) in
print("用户注册\(signed ? "成功" : "失败")")
}).disposed(by: bag)
let tap = UITapGestureRecognizer()
tap.rx.event
.subscribe(onNext: { [weak self] (tap) in
self?.view.endEditing(true)
}).disposed(by: bag)
view.addGestureRecognizer(tap)
使用Driver的实现方案
使用Driver的实现方式与Observe完全是一样的,基于Driver以下特点:
- 不会出错
- 观察发生在主调度程序上
- 有共享元素效果(share(replay: 1, scope: .whileConnected))
在创建表示UI状态的序列时,使用Driver或者用asDriver操作符转化为Driver,就可以满足observeOn
、catchErrorJustReturn
、share
这三个操作符的效果,简化编码。
Driver序列的绑定是使用drive操作符。
总结
- Model层暴露服务并负责提供程序的业务逻辑实现。
- ViewModel层表示程序的视图状态(view-state)。同时响应用户交互及来自Model层的事件,两者都受view-state变化的影响。
- View层很薄,只提供ViewModel状态的显示及输出用户交互事件。