Swift - RxSwift的使用詳解54(一個用戶注冊樣例1:基本功能實現)

本文同樣是一個 MVVM 架構的樣例(使用 Driver)。主要演示的是輸入內容的條件驗證,以及驗證結果與視圖的綁定。

1,效果圖

(1)這個是官方的演示樣例(我稍作修改),主要用來模擬用戶注冊流程。

(2)默認“注冊”按鈕不可用,只有用戶名、密碼、再次輸入密碼三者都符合如下條件時才可用:

  • 輸入用戶名時會同步檢查該用戶名是否符合條件(只能為數字或字母),以及是否已存在(網絡請求),并在輸入框下方顯示驗證結果。
  • 輸入密碼時會檢查密碼是否符合條件(最少要 5 位),并在輸入框下方顯示驗證結果。
  • 再次輸入密碼時會檢查兩個密碼是否一致,并在輸入框下方顯示驗證結果。

(3)當所有輸入都符合條件時,點擊“注冊”按鈕發起請求,并將結果彈出顯示。

2,頁面設計

(1)首先我們在 storyboard 中添加 3 個輸入框、3 個文本標簽,它們分別用于輸入用戶名、密碼、確認密碼,以及對應的驗證結果顯示。

(2)接著在界面最下方添加一個按鈕用于注冊。

(3)最后將這個 7 個 UI 控件與代碼做 @IBOutlet 關聯。

3,網絡請求服務

我們首先將需要調用的網絡請求:驗證用戶名是否存在,用戶注冊封裝起來(GitHubNetworkService.swift),方便后面使用。(這里使用最簡單的 URLSession 請求數據,大家也可參考我之前的文章改用 Moya

import Foundation
import RxSwift
 
//GitHub網絡請求服務
class GitHubNetworkService {
     
    //驗證用戶是否存在
    func usernameAvailable(_ username: String) -> Observable<Bool> {
        //通過檢查這個用戶的GitHub主頁是否存在來判斷用戶是否存在
        let url = URL(string: "https://github.com/\(username.URLEscaped)")!
        let request = URLRequest(url: url)
        return URLSession.shared.rx.response(request: request)
            .map { pair in
                //如果不存在該用戶主頁,則說明這個用戶名可用
                return pair.response.statusCode == 404
            }
            .catchErrorJustReturn(false)
    }
     
    //注冊用戶
    func signup(_ username: String, password: String) -> Observable<Bool> {
        //這里我們沒有真正去發起請求,而是模擬這個操作(平均每3次有1次失敗)
        let signupResult = arc4random() % 3 == 0 ? false : true
        return Observable.just(signupResult)
            .delay(1.5, scheduler: MainScheduler.instance) //結果延遲1.5秒返回
    }
}
 
//擴展String
extension String {
    //字符串的url地址轉義
    var URLEscaped: String {
        return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
    }
}

4,用戶注冊驗證服務

(1)首先定義一個用于表示驗證結果和信息的枚舉(ValidationResult),后面我們會將它作為驗證結果綁定到界面上。

import UIKit
 
//驗證結果和信息的枚舉
enum ValidationResult {
    case validating  //正在驗證中s
    case empty  //輸入為空
    case ok(message: String) //驗證通過
    case failed(message: String)  //驗證失敗
}
 
//擴展ValidationResult,對應不同的驗證結果返回驗證是成功還是失敗
extension ValidationResult {
    var isValid: Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }
}
 
//擴展ValidationResult,對應不同的驗證結果返回不同的文字描述
extension ValidationResult: CustomStringConvertible {
    var description: String {
        switch self {
        case .validating:
            return "正在驗證..."
        case .empty:
            return ""
        case let .ok(message):
            return message
        case let .failed(message):
            return message
        }
    }
}
 
//擴展ValidationResult,對應不同的驗證結果返回不同的文字顏色
extension ValidationResult {
    var textColor: UIColor {
        switch self {
        case .validating:
            return UIColor.gray
        case .empty:
            return UIColor.black
        case .ok:
            return UIColor(red: 0/255, green: 130/255, blue: 0/255, alpha: 1)
        case .failed:
            return UIColor.red
        }
    }
}

(2)接著將用戶名、密碼等各種需要用到的驗證封裝起來(GitHubSignupService.swift),方便后面使用。(返回的就是上面定義的 ValidationResult

import UIKit
import RxSwift
 
//用戶注冊服務
class GitHubSignupService {
     
    //密碼最少位數
    let minPasswordCount = 5
     
    //網絡請求服務
    lazy var networkService = {
        return GitHubNetworkService()
    }()
     
    //驗證用戶名
    func validateUsername(_ username: String) -> Observable<ValidationResult> {
        //判斷用戶名是否為空
        if username.isEmpty {
            return .just(.empty)
        }
         
        //判斷用戶名是否只有數字和字母
        if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil {
            return .just(.failed(message: "用戶名只能包含數字和字母"))
        }
         
        //發起網絡請求檢查用戶名是否已存在
        return networkService
            .usernameAvailable(username)
            .map { available in
                //根據查詢情況返回不同的驗證結果
                if available {
                    return .ok(message: "用戶名可用")
                } else {
                    return .failed(message: "用戶名已存在")
                }
            }
            .startWith(.validating) //在發起網絡請求前,先返回一個“正在檢查”的驗證結果
    }
     
    //驗證密碼
    func validatePassword(_ password: String) -> ValidationResult {
        let numberOfCharacters = password.count
         
        //判斷密碼是否為空
        if numberOfCharacters == 0 {
            return .empty
        }
         
        //判斷密碼位數
        if numberOfCharacters < minPasswordCount {
            return .failed(message: "密碼至少需要 \(minPasswordCount) 個字符")
        }
         
        //返回驗證成功的結果
        return .ok(message: "密碼有效")
    }
     
    //驗證二次輸入的密碼
    func validateRepeatedPassword(_ password: String, repeatedPassword: String)
        -> ValidationResult {
        //判斷密碼是否為空
        if repeatedPassword.count == 0 {
            return .empty
        }
         
        //判斷兩次輸入的密碼是否一致
        if repeatedPassword == password {
            return .ok(message: "密碼有效")
        } else {
            return .failed(message: "兩次輸入的密碼不一致")
        }
    }
}

5,ViewModel 定義

下面是本文的重點,我們創建一個用戶注冊頁面的 ViewModel(GitHubSignupViewModel.swift),它的作用就是將用戶各種輸入行為,轉換成輸出狀態。

import RxSwift
import RxCocoa
 
class GitHubSignupViewModel {
     
    //用戶名驗證結果
    let validatedUsername: Driver<ValidationResult>
     
    //密碼驗證結果
    let validatedPassword: Driver<ValidationResult>
     
    //再次輸入密碼驗證結果
    let validatedPasswordRepeated: Driver<ValidationResult>
     
    //注冊按鈕是否可用
    let signupEnabled: Driver<Bool>
     
    //注冊結果
    let signupResult: Driver<Bool>
     
    //ViewModel初始化(根據輸入實現對應的輸出)
    init(
        input: (
        username: Driver<String>,
        password: Driver<String>,
        repeatedPassword: Driver<String>,
        loginTaps: Signal<Void>
        ),
        dependency: (
        networkService: GitHubNetworkService,
        signupService: GitHubSignupService
        )) {
         
        //用戶名驗證
        validatedUsername = input.username
            .flatMapLatest { username in
                return dependency.signupService.validateUsername(username)
                    .asDriver(onErrorJustReturn: .failed(message: "服務器發生錯誤!"))
        }
         
        //用戶名密碼驗證
        validatedPassword = input.password
            .map { password in
                return dependency.signupService.validatePassword(password)
        }
         
        //重復輸入密碼驗證
        validatedPasswordRepeated = Driver.combineLatest(
            input.password,
            input.repeatedPassword,
            resultSelector: dependency.signupService.validateRepeatedPassword)
         
        //注冊按鈕是否可用
        signupEnabled = Driver.combineLatest(
            validatedUsername,
            validatedPassword,
            validatedPasswordRepeated
        ) { username, password, repeatPassword in
            username.isValid && password.isValid && repeatPassword.isValid
            }
            .distinctUntilChanged()
         
        //獲取最新的用戶名和密碼
        let usernameAndPassword = Driver.combineLatest(input.username, input.password) {
            (username: $0, password: $1) }
         
        //注冊按鈕點擊結果
        signupResult = input.loginTaps.withLatestFrom(usernameAndPassword)
            .flatMapLatest { pair in
                return dependency.networkService.signup(pair.username,
                                                        password: pair.password)
                    .asDriver(onErrorJustReturn: false)
        }
    }
}

6,ViewModel 與視圖的綁定

(1)首先為了讓 ValidationResult 能綁定到 label 上,我們要對 UILabel 進行擴展(BindingExtensions.swift

import UIKit
import RxSwift
import RxCocoa
 
//擴展UILabel
extension Reactive where Base: UILabel {
    //讓驗證結果(ValidationResult類型)可以綁定到label上
    var validationResult: Binder<ValidationResult> {
        return Binder(base) { label, result in
            label.textColor = result.textColor
            label.text = result.description
        }
    }
}

(2)最后在主視圖控制器中使用 ViewModel 進行綁定即可。

import UIKit
import RxSwift
import RxCocoa
 
class ViewController: UIViewController {
    //用戶名輸入框、以及驗證結果顯示標簽
    @IBOutlet weak var usernameOutlet: UITextField!
    @IBOutlet weak var usernameValidationOutlet: UILabel!
     
    //密碼輸入框、以及驗證結果顯示標簽
    @IBOutlet weak var passwordOutlet: UITextField!
    @IBOutlet weak var passwordValidationOutlet: UILabel!
     
    //重復密碼輸入框、以及驗證結果顯示標簽
    @IBOutlet weak var repeatedPasswordOutlet: UITextField!
    @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!
     
    //注冊按鈕
    @IBOutlet weak var signupOutlet: UIButton!
     
    let disposeBag = DisposeBag()
     
    override func viewDidLoad() {
        super.viewDidLoad()
         
        //初始化ViewModel
        let viewModel = GitHubSignupViewModel(
            input: (
                username: usernameOutlet.rx.text.orEmpty.asDriver(),
                password: passwordOutlet.rx.text.orEmpty.asDriver(),
                repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asDriver(),
                loginTaps: signupOutlet.rx.tap.asSignal()
            ),
            dependency: (
                networkService: GitHubNetworkService(),
                signupService: GitHubSignupService()
            )
        )
         
        //用戶名驗證結果綁定
        viewModel.validatedUsername
            .drive(usernameValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)
         
        //密碼驗證結果綁定
        viewModel.validatedPassword
            .drive(passwordValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)
         
        //再次輸入密碼驗證結果綁定
        viewModel.validatedPasswordRepeated
            .drive(repeatedPasswordValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)
         
        //注冊按鈕是否可用
        viewModel.signupEnabled
            .drive(onNext: { [weak self] valid  in
                self?.signupOutlet.isEnabled = valid
                self?.signupOutlet.alpha = valid ? 1.0 : 0.3
            })
            .disposed(by: disposeBag)
         
        //注冊結果綁定
        viewModel.signupResult
            .drive(onNext: { [unowned self] result in
                self.showMessage("注冊" + (result ? "成功" : "失敗") + "!")
            })
            .disposed(by: disposeBag)
    }
     
    //詳細提示框
    func showMessage(_ message: String) {
        let alertController = UIAlertController(title: nil,
                                                message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "確定", style: .cancel, handler: nil)
        alertController.addAction(okAction)
        self.present(alertController, animated: true, completion: nil)
    }
}

RxSwift使用詳解系列
原文出自:www.hangge.com轉載請保留原文鏈接

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 徐志摩:我將于茫茫人海中訪我唯一靈魂之伴侶;得之,我幸;不得,我命。 沈從文:我行過許多地方的橋,看過許多次數的云...
    胡潤民原創詩詞閱讀 211評論 0 0
  • 昨天看到了一副鋼筆淡彩,立馬愛上了。一直對這種樹偏愛,滄桑勁道!我嘗試了一下,也很有意思。我是不是有點太發散啊,自...
    荷香閱讀 252評論 0 0
  • YYLabel 繼承View,功能更加強大,支持所有UILabel的特性 1.可以實現垂直文字文字布局 @prop...
    給傷的你我依然喜歡閱讀 3,578評論 1 2