GitHubSignup 是一个注册例子的 Demo ,同时也是一个 MVVM 的 Demo 。但本节将重点介绍代码上为什么这样写,你可以从中了解到何时在代码中用 Rx 处理异步,如何合理的书写代码,以及如何优雅地处理网络请求状态。
事实上这个例子处理网络请求的方式是使用 using
操作符 hook 网络请求 Observable
的生命周期。
代码均在 RxExample 项目中,相关涉及文件如下:
我们先来简单思考一下注册需要注意哪几个点,这里主要是表单验证问题:
从 Protocols.swift
文件入手,这个文件有两个枚举 ValidationResult
和 SignupState
,两个协议 GitHubAPI
和 GitHubValidationService
。
ValidationResult
包含了四个验证结果:
enum ValidationResult {
case ok(message: String)
case empty
case validating
case failed(message: String)
}
分别是验证成功、验证为空、正在验证、验证失败。
在验证成功和验证失败两种情况中,会带上一个消息供展示。
SignupState
用于标记注册状态,表示是否已经注册,代码如下:
enum SignupState {
case signedUp(signedUp: Bool)
}
协议 GitHubAPI
和 GitHubValidationService
代码如下:
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
}
在讨论这段代码的设计前,我们先思考一下哪些是异步场景:
而验证密码和验证重复输入密码都可以同步地形式进行。
在设计到检查用户名和注册时,应当返回一个 Observable
代替 callback
,而密码的验证只需要在一个方法中返回验证结果即可。
所以上述两个协议中 usernameAvailable
、 signup
和 validateUsername
都是异步事件,都应当返回 Observable
。
DefaultImplementations.swift
文件给出了上述两个协议的实现,先来看 GitHubDefaultAPI
:
class GitHubDefaultAPI : GitHubAPI {
let URLSession: Foundation.URLSession
static let sharedAPI = GitHubDefaultAPI(
URLSession: Foundation.URLSession.shared
)
init(URLSession: Foundation.URLSession) {
self.URLSession = URLSession
}
func usernameAvailable(_ username: String) -> Observable<Bool> {
// this is ofc just mock, but good enough
let url = URL(string: "https://github.com/\(username.URLEscaped)")!
let request = URLRequest(url: url)
return self.URLSession.rx.response(request: request)
.map { (response, _) in
return response.statusCode == 404
}
.catchErrorJustReturn(false)
}
func signup(_ username: String, password: String) -> Observable<Bool> {
// this is also just a mock
let signupResult = arc4random() % 5 == 0 ? false : true
return Observable.just(signupResult)
.concat(Observable.never())
.throttle(0.4, scheduler: MainScheduler.instance)
.take(1)
}
}
方法 usernameAvailable
验证了用户名是否可用,这里验证的方案是请求该用户名对应的主页,返回 404
说明没有该用户。
signup
是一个带延时的 mock 方法,对每一次的注册返回一个随机结果,并对该结果延迟 0.4s 。
你可能会问代码
.concat(Observable.never())
存在的意义,回顾操作符 throttle ,当接到 completed 时,立即传递 completed。而 just 发射第一个值后立即发射 completed ,从而没有延时效果。当 concat 一个 never 时,Observable
永远不会发射 completed ,从而得到延时效果。
来看 GitHubDefaultValidationService
,GitHubDefaultValidationService
提供了用户名验证、密码验证、重复密码验证 三个功能。
我们只需关注方法 validateUsername
:
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)
}
首先验证输入的用户名是否为空,为空则直接返回 .just(.empty)
,再验证输入的用户名是否均为数字或父母,不是则直接返回 .just(.failed(message: "Username can only contain numbers or digits"))
。
当通过以上两种验证时,我们需要请求服务器验证用户名是否重复。.startWith(loadingValue)
为我们请求数据时添加了 loading 状态。
本节示例在代码上使用 Observable
和 Driver
区别不大,以使用 Observable
代码为例。
GithubSignupViewModel1
是对应的ViewModel。
使用 using
操作符可以创建一个和 Observable
相同生命周期的实例对象·。
当 subscribe 时,创建该实例,当 dispose 时,调用该实例的dispose。
extension Observable where Element {
public static func using<R: Disposable>(_ resourceFactory: @escaping () throws -> R, observableFactory: @escaping (R) throws -> Observable<E>) -> Observable<E>
}
在 resourceFactory
中传入一个工厂方法,返回一个可以 dispose
的实例。
在 observableFactory
中同样传入一个工厂方法,这里的 R
是 resourceFactory
中返回的实例,返回一个 Observable
,这正是与 resource 对应生命周期的 Observable
。
来看 ActivityIndicator
是如何使用 using
管理请求状态的。
extension ObservableConvertibleType {
public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<E> {
return activityIndicator.trackActivityOfObservable(self)
}
}
为 Observable
创建的扩展方法 trackActivity
中传入一个 ActivityIndicator
就可以跟踪加载状态了。
ActivityIndicator
服从协议 SharedSequenceConvertibleType
,直接调用 asObservable()
即可获取 loading 状态。
移除保证线程安全部分代码,ActivityIndicator
代码如下:
public class ActivityIndicator : SharedSequenceConvertibleType {
public typealias E = Bool
public typealias SharingStrategy = DriverSharingStrategy
private let _variable = Variable(0)
private let _loading: SharedSequence<SharingStrategy, Bool>
public init() {
_loading = _variable.asDriver()
.map { $0 > 0 }
.distinctUntilChanged()
}
fileprivate func trackActivityOfObservable<O: ObservableConvertibleType>(_ source: O) -> Observable<O.E> {
return Observable.using({ () -> ActivityToken<O.E> in
self.increment()
return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
}) { t in
return t.asObservable()
}
}
private func increment() {
_variable.value = _variable.value + 1
}
private func decrement() {
_variable.value = _variable.value - 1
}
public func asSharedSequence() -> SharedSequence<SharingStrategy, E> {
return _loading
}
}
我们通过 _variable
表示正在执行的 Observable
,当 _variable
中的值为 0 时,_loading
发射一个 false
,表示加载结束,当 _variable
中的值大于 0 时,_loading
会发射 true
。
方法 increment
和 decrement
处理的在执行的 Observable
的数量。
而在 trackActivityOfObservable
中使用了 using
将 increment
和 decrement
与 Observable
的生命周期绑定起来。
调用 using
的 resourceFactory
时,调用 increment
将资源数加1。
当 dispose 时,调用 ActivityToken
的 dispose
方法。
ActivityToken
代码如下:
private struct ActivityToken<E> : ObservableConvertibleType, Disposable {
private let _source: Observable<E>
private let _dispose: Cancelable
init(source: Observable<E>, disposeAction: @escaping () -> ()) {
_source = source
_dispose = Disposables.create(with: disposeAction)
}
func dispose() {
_dispose.dispose()
}
func asObservable() -> Observable<E> {
return _source
}
}
这就完成了对 Observable
的监听,使用 trackActivity
可以监听任何一个 Observable
的生命周期,当我们需要依次发两个请求时,可以写类似如下代码:
let activity = ActivityIndicator()
someRequest()
.flatMap(secondeRequest)
.trackActivity(activity)
此时 activity
监听的是 someRequest().flatMap(secondeRequest)
Observable ,当该 Observable 结束时,我们才会从 activity 中获取到请求结束状态。你可以很容易地在你的代码中插入各种状态监听,这不会对你的逻辑代码有任何影响。