RxExample GitHubSignup 部分代碼解讀

FauS76 7年前發布 | 8K 次閱讀 工廠模式 iOS開發

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/

 

 本文由用戶 FauS76 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!