代码下载
由于弹框属于View层,但是又得在ViewModel中使用,这违背了MVVM模式中ViewModel不能引用View的限制。所以通过协议来解决这个问题,在ViewModel层中定义如下协议:
protocol WireFrame {
/// 弹框
/// - Parameters:
/// - title: 标题
/// - message: 信息
/// - cancelAction: 取消按钮
/// - actions: 其他按钮数组
/// - animated: 是否带动画
/// - completion: 完成闭包
func promptFor(_ title: String, message: String, cancelAction: Action, actions: [Action]?, animated: Bool, completion: (() -> Void)?) -> Observable
}
在View层中定义一个DefaultWireFrame类实现上面的协议:
class DefaultWireFrame: WireFrame {
func promptFor(_ title: String, message: String, cancelAction: Action, actions: [Action]? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> Observable {
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)
}
})
}
}
代码分析:
onNext
发出元素present
弹出弹框,构建并返回资源清理对象Disposable
,清理资源时使用dismiss
关闭弹框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
/// 注册
/// - Parameters:
/// - username: 用户名
/// - password: 密码
func signup(_ username: String, password: String) -> Observable
}
/// GitHub数据是否有效接口
protocol GitHubValidationService {
/// 判断用户名是否有效
/// - Parameter username: 用户名
func validateUsername(_ username: String) -> Observable
/// 判断密码是否有效
/// - 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 {
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 {
// 四分之一的几率失败
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 {
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: "密码不同")
}
}
}
代码分析:
ValidationResult
类型,并使用startWith
操作符设置一个初始元素首先定义一个ActivityToken:
/// 活动令牌
struct ActivityToken: ObservableConvertibleType, Disposable {
let _source: Observable
let _dispose: Cancelable
init(source: Observable, disposeAction: @escaping () -> Void) {
_source = source
_dispose = Disposables.create(with: disposeAction)
}
func asObservable() -> Observable {
return _source
}
func dispose() {
_dispose.dispose()
}
}
ActivityToken遵守Disposable、ObservableConvertibleType协议,也就是说ActivityToken既是Disposable也是Observable。实际上是用来存储一个Observable和Disposable清理资源的闭包。
定义ActivityIndicator:
/// 活动指示器
class ActivityIndicator: SharedSequenceConvertibleType {
typealias Element = Bool
typealias SharingStrategy = DriverSharingStrategy
/// 锁
let lock = NSRecursiveLock()
/// 计数序列
let relay = BehaviorRelay(value: 0)
/// 加载序列
let loading: SharedSequence
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: Source) -> Observable {
return Observable.using({ [weak self] () -> ActivityToken in
// 增量计数
self?.increment()
// 返回一个Disposable
return ActivityToken(source: source.asObservable(), disposeAction: self?.decrement ?? {})
}, observableFactory: { (t) in
// 返回一个序列
t.asObservable()
})
}
/// 遵守协议
func asSharedSequence() -> SharedSequence {
return loading
}
}
ActivityIndicator的实现思想类似于内存管理中的引用计数,通过increment/decrement这两个函数来增/减计数的值,再使用BehaviorRelay把这些计数值作为元素发送出来,最后通过map操作符将元素转换为转化为BOOL类型的序列。
trackActivityOfObservable函数实现:
扩展ObservableConvertibleType协议,方便Observable序列记录活动状态:
extension ObservableConvertibleType {
func trackActivity(_ indicator: ActivityIndicator) -> Observable {
return indicator.trackActivityOfObservable(self)
}
}
MVVM模式的核心是ViewModel,它是一种特殊的model类型,用于表示程序的UI状态,包含描述每个UI控件的状态,所有的UI逻辑都在ViewModel中。
实际上ViewModel暴露属性来表示UI状态,它同样暴露命令来表示UI操作(通常是方法)。ViewModel负责管理基于用户交互的UI状态的改变。
在ViewModel层创建SignupObservableVM,并定义如下序列来表示各种UI状态:
class SignupObservableVM {
// 用户名有效验证的序列
let validatedUsername: Observable
// 密码有效验证的序列
let validatedPassword: Observable
// 重复密码有效验证的序列
let validatedRepeatedPassword: Observable
// 允许注册的序列
let signupEnabled: Observable
// 注册的序列
let signedIn: Observable
// 注册中的序列
let signingIn: Observable
初始化ViewModel,接收View层的事件:
/// 初始化
/// - Parameters:
/// - input: 输入序列元组
/// - dependency: 依赖的功能模型
init(
input: (
username: Observable,// 用户名输入序列
password: Observable,// 密码输入序列
repeatedPassword: Observable,// 二次密码输入序列
signTaps: Observable),// 注册点击序列
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)
代码分析:
不在登录中,其他全部有效
的BOOL值返回实现表示是否注册成功的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 in
let message = loggedIn ? "GitHub注册成功" : "GitHub注册失败"
return DefaultWireFrame()
.promptFor("提示", message: message, cancelAction: "确定", actions: ["否"])
.map({ _ in loggedIn })
}).share(replay: 1)
代码分析:
将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的实现方式与Observe完全是一样的,基于Driver以下特点:
在创建表示UI状态的序列时,使用Driv或者用asDriver操作符转化为Driver,就可以满足observeOn
、catchErrorJustReturn
、share
这三个操作符的效果,简化编码。
Driver序列的绑定是使用drive操作符。