RxSwift 實戰操作【註冊登入】
前言
看了前面的文章,相信很多同學還不知道RxSwift
該怎麼使用,這篇檔案將帶領大家一起寫一個 註冊登入(ps:本例子採用MVVM
)的例子進行實戰。本篇文章是基於RxSwift3.0
寫的,採用的是Carthage
第三方管理工具匯入的RxSwift3.0
,關於Carthage
的安裝和使用,請參考Carthage的安裝和使用。
最終效果
下載Demo點我
前提準備
首先請大家新建一個swift
工程,然後把RxSwift
引入到專案中,然後能夠編譯成功就行。
然後我們來分析下各個介面的需求:
註冊介面需求:
- 輸入使用者名稱必須大於等於6個字元,不然密碼不能輸入;
- 密碼必須大於等於6個字元,不然重複密碼不能輸入;
- 重複密碼和密碼必須一樣, 不能註冊按鈕不能點選;
- 點選註冊按鈕,提示註冊成功或者註冊失敗;
- 註冊成功會寫進本地的plist檔案,然後輸入使用者名稱會檢測該使用者名稱是否已註冊
登入介面需求:
- 點選輸入使用者名稱,檢測是否已存在,如果存在,戶名可用,否則提示使用者名稱不存在;
- 輸入密碼,點選登入,如果密碼錯則提示密碼錯誤,否則進入列表介面,提示登入成功。
列表介面需求:
- 輸入聯絡人的首字母進行篩選
好了,分析完上面的需求之後,是時候展示真正的技術了,let's go。
註冊介面
大家現在storyboard
中建立出下面這個樣子的介面(ps:新增約束不在本篇範圍內):
建立對應的檔案
然後建立一個對應的控制器RegisterViewController
RegisterViewModel.swift
,將RegisterViewController
與storyboard
中的控制器關聯,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
類,最好不要繼承NSObject
,Swift
中推薦儘量使用原生類。我們考慮到當文字框內容變化的時候,我們需要把文字框的內容當做引數傳遞進來進行處理,判斷是否符合我們的要求,然後返回處理結果,也就是狀態。基於此,我們建立一個Protocol.swift
檔案,建立一個enum
用於表示我們處理結果,所以,我們在Protocol.swift
檔案中新增如下程式碼:
enum Result {
case ok(message:String)
case empty
case failed(message:String)
}
username處理
先寫出總結:其實就是兩個流的傳遞過程。
UI操作 -> ViewModel -> 改變資料
資料改變 -> ViewModel -> UI重新整理
回到我們Service
中ValidationService
類中,寫一個檢測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.orEmpty
是RxCocoa
庫中的東西,它把TextFiled
的text
變成了一個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)
- 將
ViewModel
中username
處理結果usernameUseable
繫結到nameLabel
顯示文案上,根據不同的結果顯示不同的文案; - 將
ViewModel
中username
處理結果usernameUseable
繫結到pwdTextField
,根據不同的結果判斷是否可以輸入。
關於上面的validationResult
和inputEnabled
是需要我們自己去定製的,這就用到了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
結果,進行它的text
和textColor
顯示 - 我們對
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()
中初始化passwordUseable
和rePasswordUseable
:
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)