一行一行看RxExample中的GitHubSignup例子
前言: RxSwift学习阶段总结一些心得,这几天看了RxExample
中的GitHubSignup
例子(我觉得,这个学习RxSwift很不错),由于水平有限,错误之处,还请指正.直接开始看demo吧,💊💊,切克闹~
很简单,只有几个Swift文件,不过你可能会疑惑,为什么UsingDriver和UsingVanilaObservables两个文件那么类似?这么问的童鞋,恭喜你,你该制杖了
很明显,本demo是用老司机(Driver)和Observables两个版本实现的嘛!😅
好了,进入正题
先说下Protocols吧,POP和Observables结合的最完美案例:
首先映入眼帘的两个枚举,不必多说,一个是验证结果,一个是注册状态
enum ValidationResult {
case ok(message: String)
case empty
case validating
case failed(message: String)
}
enum SignupState {
case signedUp(signedUp: Bool)
}
extension ValidationResult {
var isValid: Bool {
switch self {
case .ok:
return true
default:
return false
}
}
}
这里两个协议
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) -> ValidationResult
func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult
}
不用多说了,肯定是验证用户名,密码相关的,且看一下我们的viewModel: GithubSignupViewModel1
(这里先介绍Observables版本的)
// outputs {
let validatedUsername: Observable<ValidationResult>
let validatedPassword: Observable<ValidationResult>
let validatedPasswordRepeated: Observable<ValidationResult>
// Is signup button enabled
let signupEnabled: Observable<Bool>
// Has user signed in
let signedIn: Observable<Bool>
// Is signing process in progress
let signingIn: Observable<Bool>
// }
很明显,viewModel要输出的各种验证结果,聪明的你已经猜到了,他的初始化方法肯定会有个叫做input
的参数😝,
init(input: (
username: Observable<String>,
password: Observable<String>,
repeatedPassword: Observable<String>,
loginTaps: Observable<Void>
),
dependency: (
API: GitHubAPI,
validationService: GitHubValidationService,
wireframe: Wireframe
)
) { ... }
username -> 用户名输入结果
password -> 密码输入结果
repeatedPassword -> 重复输入密码结果
loginTaps -> 登录按钮的点击结果
它们被打包成一个元组作为初始化函数的参数,而另外一个依赖参数dependency则包含了我们上面提到的两个协议类型.
output的三个参数是这样被赋值的:
validatedUsername = input.username
.throttle(1, scheduler: MainScheduler.instance)
.flatMapLatest { username in
return validationService.validateUsername(username)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(.failed(message: "Error contacting server"))
}
.shareReplay(1)
throttle: 信号发射的最短时长(保证不会因为用户操作太快而频繁的发射信号)
flatMapLatest: 这里要说一下了,它仅仅执行最新的信号,如果有新的信号进来的时候,取消上一次未执行完的整个序列,参考 map + switchLatest.所以,这里只会验证用户最新输入的用户名(要考虑到用户随时修改输入的文字嘛~)
shareReplay: 在这里已经讲的很明白了~
至于flatMapLatest中的validationService.validateUsername
,就是之前提到的协议(GitHubValidationService
)的方法的实现了,代码如下:
func validateUsername(_ username: String) -> Observable<ValidationResult> {
if username.characters.count == 0 {
return .just(.empty)
}
// this obviously won't be
if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil {
return .just(.failed(message: "Username can only contain numbers or digits"))
}
let loadingValue = ValidationResult.validating
return API
.usernameAvailable(username)
.map { available in
if available {
return .ok(message: "Username available")
}
else {
return .failed(message: "Username already taken")
}
}
.startWith(loadingValue)
}
验证用户名那一套,首先看长度是不是为空,然后看看是否符合用户名格式,再然后调用GitHubAPI
的usernameAvailable
方法来验证后台接口是否包含了用户输入的这个userName,so easy~这里,重点来了:
let usernameAndPassword = Observable.combineLatest(input.username, input.password) { ($0, $1) }
signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
.flatMapLatest { (username, password) in
return API.signup(username, password: password)
.trackActivity(signingIn)
.asDriver(onErrorJustReturn: false)
}
.flatMapLatest { loggedIn -> Driver<Bool> in
let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
return wireframe.promptFor(message, cancelAction: "OK", actions: [])
// propagate original value
.map { _ in
loggedIn
}
.asDriver(onErrorJustReturn: false)
}
首先,withLatestFrom
很陌生的一个操作符:
每当第一条序列发射一个元素的时候,使用第二条序列的最新元素将两个可观察序列合并成一个可观察序列.听起来很抽象,结合例子中的实际场景来看:第一条序列就是登录按钮的点击事件,他每点击一次,就回去查看另一条序列usernameAndPassword,如果usernameAndPassword有新值了,发送一个新的登录信息.说白了,就是热信号监听冷信号,传说中的 冰火两重天
注意: 我们的viewModel里面并没有observables的订阅行为,这一切都只是单纯的把输入的序列转换成输出的序列~
viewModel配置好了,那么接下来进入GitHubSignupViewController1
,订阅这些observables吧!
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.sharedInstance
)
)
代码大意很容易理解,但是我要解释下 orEmpty
,一般文本框的text后面都会加上这个操作符的,它的作用就是把text的Optional类型的value值转换成String类型.
接下来就可以对viewModel的一系列Observables做绑定操作了.
viewModel.signupEnabled
.subscribe(onNext: { [weak self] valid in
self?.signupOutlet.isEnabled = valid
self?.signupOutlet.alpha = valid ? 1.0 : 0.5
})
.addDisposableTo(disposeBag)
viewModel.validatedUsername
.bindTo(usernameValidationOutlet.rx.validationResult)
.addDisposableTo(disposeBag)
作为RxSwift的基础学习,还是有必要解释下的:
bindTo,顾名思义,就是把Observabel的value值绑定到UI上面,类似于subscribe行为.这里问题又来了,你好像没有见过这个家伙validationResult
extension Reactive where Base: UILabel {
var validationResult: UIBindingObserver<Base, ValidationResult> {
return UIBindingObserver(UIElement: base) { label, result in
label.textColor = result.textColor
label.text = result.description
}
}
}
为UILabel拓展一个rx属性,我们可以直接把结果绑定到validationResult上,不然只能这样了:
viewModel.validatedUsername
.subscribe(onNext: { [weak self] valid in
self?.usernameValidationOutlet.textColor = valid.textColor
self?.usernameValidationOutlet.text = valid.description
})
.addDisposableTo(disposeBag)
呃,这显然可读性很差,很蛋疼...
到这里,demo差不多介绍完了,至于另一个老司机(Driver
)版本,就不做过多解释了,想要了解区别的话,参照文档.
结尾语:由于小弟水平有限,分享的也都是比较基础的知识,这些也都是小弟学习期间的笔记,若是哪位大佬无意间看到了,也请指点小弟一二,共同学习下~~
网友评论