1. 程式人生 > >RxSwift 實戰操作【註冊登入】

RxSwift 實戰操作【註冊登入】

前言

看了前面的文章,相信很多同學還不知道RxSwift該怎麼使用,這篇檔案將帶領大家一起寫一個 註冊登入(ps:本例子採用MVVM)的例子進行實戰。本篇文章是基於RxSwift3.0寫的,採用的是Carthage第三方管理工具匯入的RxSwift3.0,關於Carthage的安裝和使用,請參考Carthage的安裝和使用。

最終效果

效果圖

下載Demo點我

前提準備

首先請大家新建一個swift工程,然後把RxSwift引入到專案中,然後能夠編譯成功就行。

然後我們來分析下各個介面的需求:

註冊介面需求:

  • 輸入使用者名稱必須大於等於6個字元,不然密碼不能輸入;
  • 密碼必須大於等於6個字元,不然重複密碼不能輸入;
  • 重複密碼和密碼必須一樣, 不能註冊按鈕不能點選;
  • 點選註冊按鈕,提示註冊成功或者註冊失敗;
  • 註冊成功會寫進本地的plist檔案,然後輸入使用者名稱會檢測該使用者名稱是否已註冊

登入介面需求:

  • 點選輸入使用者名稱,檢測是否已存在,如果存在,戶名可用,否則提示使用者名稱不存在;
  • 輸入密碼,點選登入,如果密碼錯則提示密碼錯誤,否則進入列表介面,提示登入成功。

列表介面需求:

  • 輸入聯絡人的首字母進行篩選

好了,分析完上面的需求之後,是時候展示真正的技術了,let's go。

註冊介面

大家現在storyboard中建立出下面這個樣子的介面(ps:新增約束不在本篇範圍內):
圖1

建立對應的檔案

然後建立一個對應的控制器RegisterViewController

類,另外建立一個RegisterViewModel.swift,將RegisterViewControllerstoryboard中的控制器關聯,RegisterViewController看起來應該是這樣子的:

class RegisterViewController: UIViewController {
    @IBOutlet weak var userNameTextField: UITextField!
    @IBOutlet weak var nameLabel: UILabel!
    
    @IBOutlet weak var pwdTextField: UITextField!
    @IBOutlet weak var pwdLabel: UILabel!
    
    @IBOutlet weak var rePwdTextField: UITextField!
    @IBOutlet weak var rePwdLabel: UILabel!
    
    @IBOutlet weak var registButton: UIButton!
    @IBOutlet weak var loginButton: UIBarButtonItem!
        
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

另外,我們建立一個Service.swift檔案。
Service檔案主要負責一些網路請求,和一些資料訪問的操作。然後供ViewModel使用,由於本次實戰沒有使用到網路,所以我們只是模擬從本地plist檔案中讀取使用者資料。

首先我們在Service檔案中建立一個ValidationService類,最好不要繼承NSObjectSwift中推薦儘量使用原生類。我們考慮到當文字框內容變化的時候,我們需要把文字框的內容當做引數傳遞進來進行處理,判斷是否符合我們的要求,然後返回處理結果,也就是狀態。基於此,我們建立一個Protocol.swift檔案,建立一個enum用於表示我們處理結果,所以,我們在Protocol.swift檔案中新增如下程式碼:

enum Result {
    case ok(message:String)
    case empty
    case failed(message:String)
}

username處理

先寫出總結:其實就是兩個流的傳遞過程。
UI操作 -> ViewModel -> 改變資料
資料改變 -> ViewModel -> UI重新整理

回到我們ServiceValidationService類中,寫一個檢測username的方法。它看起來應該是這個樣子的:

class ValidationService {
    
    // 單例類
    static let instance = ValidationService()
    private init(){}
    
    let minCharactersCount = 6
    
    func validationUserName(_ name:String) -> Observable<Result> {
        if name.characters.count == 0 { // 當字串為空的時候,什麼也不做
            return Observable.just(Result.empty)
        }
        
        if name.characters.count < minCharactersCount {
            return Observable.just(Result.failed(message: "使用者名稱長度至少為6位"))
        }
        
        if checkHasUserName(name) {
            return Observable.just(Result.failed(message: "使用者名稱已存在"))
        }
        
        return Observable.just(Result.ok(message: "使用者名稱可用"))
    }
    
    func checkHasUserName(_ userName:String) -> Bool {
        let filePath = NSHomeDirectory() + "/Documents/users.plist"
        guard let userDict = NSDictionary(contentsOfFile: filePath) else {
            return false
        }
        
        let usernameArray = userDict.allKeys as NSArray
        
        return usernameArray.contains(userName)
    }
}

接下來該處理我們的RegisterViewModel了,我們宣告一個username,指定為Variable型別,為什麼是一個Variable型別?因為它既是一個Observer,又是一個Observable,所以我們宣告它是一個Variable型別的物件。我們對username處理應該會有一個結果,這個結果應該是由介面監聽來改變介面顯示,因此我們宣告一個usernameUseable表示對username處理的一個結果,因為它是一個Observable,所以我們將它宣告為Observable型別的物件,所以RegisterViewModel看起來應該是這樣子的:

class RegisterViewModel {
    let username = Variable<String>("")
    
    let usernameUseable:Observable<Result>
    
    init() {
    }
}

然後我們再寫RegisterViewController,它看起來應該是這樣子的:

private let disposeBag = DisposeBag()

override func viewDidLoad() {
    super.viewDidLoad()

    let viewModel = RegisterViewModel()
        
    userNameTextField.rx.text.orEmpty.bind(to: viewModel.username).disposed(by: disposeBag)
}
  • 其中userNameTextField.rx.text.orEmptyRxCocoa庫中的東西,它把TextFiledtext變成了一個Observable,後面的orEmpty我們可以Command點進去看下,它會把String?過濾nil幫我們變為String型別。
  • bind(to:viewModel.username)的意思是viewModel.username作為一個observer(觀察者)觀察userNameTextField上的內容變化。
  • 因為我們有監聽,就要有監聽資源的回收,所以我們建立一個disposeBag來盛放我們這些監聽的資源。

現在,回到我們的RegisterViewModel中,我們新增如下程式碼:

init() {
    let service = ValidationService.instance
        
    usernameUseable = username.asObservable().flatMapLatest{ username in
        return service.validationUserName(username).observeOn(MainScheduler.instance).catchErrorJustReturn(.failed(message: "userName檢測出錯")).shareReplay(1)
    }
}
  • viewModel中,我們把username當做observable(被觀察者),然後對裡面的元素進行處理之後發射對應的事件。

下面我們在RegisterViewController中處理我們的username請求結果。我們在ViewDidLoad中新增下列程式碼:

viewModel.usernameUseable.bind(to:
nameLabel.rx.validationResult).addDisposableTo(disposeBag)

viewModel.usernameUseable.bind(to:
pwdTextField.rx.inputEnabled).addDisposableTo(disposeBag)
  • ViewModelusername處理結果usernameUseable繫結到nameLabel顯示文案上,根據不同的結果顯示不同的文案;
  • ViewModelusername處理結果usernameUseable繫結到pwdTextField,根據不同的結果判斷是否可以輸入。

關於上面的validationResultinputEnabled是需要我們自己去定製的,這就用到了RxSwift 系列(九) -- 那些難以理解的概念文章中的UIBindingObserver了。

所以,我們在Protocol.swift檔案中新增如下程式碼:

extension Result {
    var isValid:Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }
}



extension Result {
    var textColor:UIColor {
        switch self {
        case .ok:
            return UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0)
        case .empty:
            return UIColor.black
        case .failed:
            return UIColor.red
        }
    }
}

extension Result {
    var description: String {
        switch self {
        case let .ok(message):
            return message
        case .empty:
            return ""
        case let .failed(message):
            return message
        }
    }
}

extension Reactive where Base: UILabel {
    var validationResult: UIBindingObserver<Base, Result> {
        return UIBindingObserver(UIElement: base) { label, result in
            label.textColor = result.textColor
            label.text = result.description
        }
    }
}

extension Reactive where Base: UITextField {
    var inputEnabled: UIBindingObserver<Base, Result> {
        return UIBindingObserver(UIElement: base) { textFiled, result in
            textFiled.isEnabled = result.isValid
        }
    }
}
  • 首先,我們對Result進行了擴充套件,添加了isValid屬性,如果狀態是ok,這個屬性就為true,否則為false
  • 然後對Result添加了一個textColor屬性,如果狀態為ok則為綠色,否則使用紅色
  • 我們對UILabel進行了UIBingObserver,根據result結果,進行它的texttextColor顯示
  • 我們對UITextField進行了UIBingObserver,根據result結果,對它的isEnabled進行設定。

寫到這裡,我們暫停一下,執行一下專案看下程式的執行情況,試著去輸入username嘗試一下效果,是不是很激動??

password處理

有了上面username的理解,相信大家對password也就熟門熟路了,因此有些細節就不做描述了。

我們現在對Service中新增對password的處理:

func validationPassword(_ password:String) -> Result {
    if password.characters.count == 0 {
        return Result.empty
    }
        
    if password.characters.count < minCharactersCount {
        return .failed(message: "密碼長度至少為6位")
    }
        
    return .ok(message: "密碼可用")
}
     
func validationRePassword(_ password:String, _ rePassword: String) -> Result {
    if rePassword.characters.count == 0 {
        return .empty
    }
        
    if rePassword.characters.count < minCharactersCount {
        return .failed(message: "密碼長度至少為6位")
    }
        
    if rePassword == password {
        return .ok(message: "密碼可用")
    }
        
    return .failed(message: "兩次密碼不一樣")
}
  • validationPassword處理我們輸入的密碼;
  • validationRePassword處理我們輸入的重複密碼;
  • 上面函式的返回值都是Result型別的值,因為我們外面不需要對這個過程進行監聽,所以不必返回一個新的序列。

RegisterViewModel中新增需要的observable

let password = Variable<String>("")
let rePassword = Variable<String>("")

let passwordUseable:Observable<Result>
let rePasswordUseable:Observable<Result>    

然後在init()中初始化passwordUseablerePasswordUseable

passwordUseable = password.asObservable().map { passWord in
    return service.validationPassword(passWord)
}.shareReplay(1)
        
rePasswordUseable = Observable.combineLatest(password.asObservable(), rePassword.asObservable()) {
    return service.validationRePassword($0, $1)
}.shareReplay(1)

回到RegisterViewController中,新增對應的繫結:

pwdTextField.rx.text.orEmpty.bind(to: viewModel.password).disposed(by: disposeBag)

rePwdTextField.rx.text.orEmpty.bind(to: viewModel.rePassword).disposed(by: disposeBag)

viewModel.passwordUseable.bind(to: pwdLabel.rx.validationResult).addDisposableTo(disposeBag)

viewModel.passwordUseable.bind(to: rePwdTextField.rx.inputEnabled).addDisposableTo(disposeBag)
        
viewModel.rePasswordUseable.bind(to: rePwdLabel.rx.validationResult).addDisposableTo(disposeBag)