RxExample GitHubSignup 部分代碼解讀
GitHubSignup 是一個注冊例子的 Demo ,同時也是一個 MVVM 的 Demo 。但本節將重點介紹代碼上 為什么這樣寫 ,你可以從中了解到何時在代碼中用 Rx 處理異步,如何合理的書寫代碼,以及如何優雅地處理網絡請求狀態。
事實上這個例子處理網絡請求的方式是使用 using 操作符 hook 網絡請求 Observable 的生命周期。
代碼均在 RxExample 項目中,相關涉及文件如下:
- GitHubSignup 文件夾所有內容
- ActivityIndicator.swift
我們先來簡單思考一下注冊需要注意哪幾個點,這里主要是表單驗證問題:
- 用戶名不能重復,需要提交用戶名到服務器驗證
- 注冊密碼有等級限制,比如長度、帶大小寫字母
- 兩次輸入的密碼相同
從 Protocols.swift 文件入手,這個文件有兩個枚舉 ValidationResult 和 SignupState ,兩個協議 GitHubAPI 和 GitHubValidationService 。
ValidationResult 包含了四個驗證結果:
enumValidationResult{
case ok(message: String)
case empty
case validating
case failed(message: String)
}
分別是驗證成功、驗證為空、正在驗證、驗證失敗。
在驗證成功和驗證失敗兩種情況中,會帶上一個消息供展示。
SignupState 用于標記注冊狀態,表示是否已經注冊,代碼如下:
enumSignupState{
case signedUp(signedUp: Bool)
}
協議 GitHubAPI 和 GitHubValidationService 代碼如下:
protocolGitHubAPI{
funcusernameAvailable(_username: String) -> Observable<Bool>
funcsignup(_username: String, password: String) -> Observable<Bool>
}
protocolGitHubValidationService{
funcvalidateUsername(_username: String) -> Observable<ValidationResult>
funcvalidatePassword(_password: String) -> ValidationResult
funcvalidateRepeatedPassword(_password: String, repeatedPassword: String) -> ValidationResult
}
在討論這段代碼的設計前,我們先思考一下哪些是異步場景:
- 檢查用戶名是否可用
- 注冊
而驗證密碼和驗證重復輸入密碼都可以同步地形式進行。
在設計到 檢查用戶名 和 注冊 時,應當返回一個 Observable 代替 callback ,而密碼的驗證只需要在一個方法中返回驗證結果即可。
所以上述兩個協議中 usernameAvailable 、 signup 和 validateUsername 都是異步事件,都應當返回 Observable 。
DefaultImplementations.swift 文件給出了上述兩個協議的實現,先來看 GitHubDefaultAPI :
classGitHubDefaultAPI:GitHubAPI{
let URLSession: Foundation.URLSession
static let sharedAPI = GitHubDefaultAPI(
URLSession: Foundation.URLSession.shared
)
init(URLSession: Foundation.URLSession) {
self.URLSession = URLSession
}
funcusernameAvailable(_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)
}
funcsignup(_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 :
funcvalidateUsername(_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 狀態。
UsingVanillaObservables > 1
本節示例在代碼上使用 Observable 和 Driver 區別不大,以使用 Observable 代碼為例。
GithubSignupViewModel1 是對應的ViewModel。
ActivityIndicator
Using 操作符
使用 using 操作符可以創建一個和 Observable 相同生命周期的實例對象·。
當 subscribe 時,創建該實例,當 dispose 時,調用該實例的dispose。
extensionObservablewhereElement{
public static funcusing<R: Disposable>(_resourceFactory: @escaping() throws -> R, observableFactory: @escaping (R) throws -> Observable<E>) -> Observable<E>
}
在 resourceFactory 中傳入一個工廠方法,返回一個可以 dispose 的實例。
在 observableFactory 中同樣傳入一個工廠方法,這里的 R 是 resourceFactory 中返回的實例,返回一個 Observable ,這正是與 resource 對應生命周期的 Observable 。

來看 ActivityIndicator 是如何使用 using 管理請求狀態的。
extensionObservableConvertibleType{
public functrackActivity(_activityIndicator: ActivityIndicator) -> Observable<E> {
return activityIndicator.trackActivityOfObservable(self)
}
}
為 Observable 創建的擴展方法 trackActivity 中傳入一個 ActivityIndicator 就可以跟蹤加載狀態了。
ActivityIndicator 服從協議 SharedSequenceConvertibleType ,直接調用 asObservable() 即可獲取 loading 狀態。
移除保證線程安全部分代碼, ActivityIndicator 代碼如下:
public classActivityIndicator: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 functrackActivityOfObservable<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 funcincrement() {
_variable.value = _variable.value + 1
}
private funcdecrement() {
_variable.value = _variable.value - 1
}
public funcasSharedSequence() -> 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 structActivityToken<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)
}
funcdispose() {
_dispose.dispose()
}
funcasObservable() -> Observable<E> {
return _source
}
}
這就完成了對 Observable 的監聽。
來自:http://blog.dianqk.org/2017/03/03/rxexample-githubsignup/