看了前面的文章,相信不少同窗還不知道RxSwift
該怎麼使用,這篇文件將帶領你們一塊兒寫一個 註冊登陸(ps:本例子採用MVVM
)的例子進行實戰。本篇文章是基於RxSwift3.0
寫的,採用的是Carthage
第三方管理工具導入的RxSwift3.0
,關於Carthage
的安裝和使用,請參考Carthage的安裝和使用。html
下載Demo點我git
首先請你們新建一個swift
工程,而後把RxSwift
引入到項目中,而後可以編譯成功就行。github
而後咱們來分析下各個界面的需求:數據庫
好了,分析完上面的需求以後,是時候展現真正的技術了,let's go。swift
你們如今storyboard
中創建出下面這個樣子的界面(ps:添加約束不在本篇範圍內):
api
而後創建一個對應的控制器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) }
先寫出總結:其實就是兩個流的傳遞過程。
UI操做 -> ViewModel -> 改變數據
數據改變 -> ViewModel -> UI刷新app
回到咱們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
嘗試一下效果,是否是很激動??
有了上面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)
😁,先放輕鬆一下,運行程序看看,輸入用戶名和密碼和重複密碼感覺一下。
首先咱們在Service
裏面添加一個註冊函數:
func register(_ username:String, password:String) -> Observable<Result> { let userDict = [username: password] if (userDict as NSDictionary).write(toFile: filePath, atomically: true) { return Observable.just(Result.ok(message: "註冊成功")) }else{ return Observable.just(Result.failed(message: "註冊失敗")) } }
我是直接把註冊信息寫入到本地的plist文件,寫入成功就返回ok,不然就是
failed。
回到RegisterViewModel
中添加以下代碼:
let registerTaps = PublishSubject<Void>() let registerButtonEnabled:Observable<Bool> let registerResult:Observable<Result>
registerTaps
咱們使用了PublishSubject
,由於不須要有初始元素,其實前面的Variable
均可以換成PublishSubject
。大夥能夠試試;registerButtonEnabled
就是註冊按鈕是否可用的輸出,這個其實關係到username
和password
;registerResult
就只最後註冊結果了.咱們在init()
函數中初始化registerButtonEnabled
和registerResult
,在init()
中添加以下代碼:
registerButtonEnabled = Observable.combineLatest(usernameUseable, passwordUseable, rePasswordUseable) { (username, password, repassword) in return username.isValid && password.isValid && repassword.isValid }.distinctUntilChanged().shareReplay(1) let usernameAndPwd = Observable.combineLatest(username.asObservable(), password.asObservable()){ return ($0, $1) } registerResult = registerTaps.asObservable().withLatestFrom(usernameAndPwd).flatMapLatest { (username, password) in return service.register(username, password: password).observeOn(MainScheduler.instance).catchErrorJustReturn(Result.failed(message: "註冊失敗")) }.shareReplay(1)
registerButtonEnabled
的處理,把username
、password
和rePassword
的處理結果綁定到一塊兒,返回一個總的結果流,這是個Bool
值的流。username
和password
組合,獲得一個元素是它倆組合的元祖的流。registerTaps
事件進行監聽,咱們拿到每個元組進行註冊行爲,涉及到耗時數據庫操做,咱們須要對這個過程進行監聽,因此咱們使用flatMap
函數,返回一個新的流。回到RegisterViewController
中,添加按鈕的綁定:
registButton.rx.tap.bind(to: viewModel.registerTaps).disposed(by: disposeBag) viewModel.registerButtonEnabled.subscribe(onNext: { [weak self](valid) in self?.registButton.isEnabled = valid self?.registButton.alpha = valid ? 1 : 0.5 }).disposed(by: disposeBag) viewModel.registerResult.subscribe(onNext: { [weak self](result) in switch result { case let .ok(message): self?.showAlert(message:message) case .empty: self?.showAlert(message:"") case let .failed(message): self?.showAlert(message:message) } }).disposed(by: disposeBag)
func showAlert(message:String) { let action = UIAlertAction(title: "肯定", style: .default) { [weak self](_) in self?.userNameTextField.text = "" self?.pwdTextField.text = "" self?.rePwdTextField.text = "" // 這個方法是基於點擊肯定讓全部元素還原才抽出的,可不搭理。 self?.setupRx() } let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) alertController.addAction(action) present(alertController, animated: true, completion: nil) }
注意:上述setupRx()
是爲了點擊肯定以後,界面上全部的元素還原才抽出的,具體的能夠查看demo
如今,運行項目,咱們已經可以正常的註冊帳號了。😊
首先咱們在storyboard
中添加登陸界面,以下,當點擊登陸的時候,就跳轉到登陸界面。
建立一個LoginViewController.swift
和LoginViewModel.swift
文件,有了上述註冊功能的講解,相信登陸功能也很容易了。
咱們在Service.swift
中添加以下代碼:
func loginUserNameValid(_ userName:String) -> Observable<Result> { if userName.characters.count == 0 { return Observable.just(Result.empty) } if checkHasUserName(userName) { return Observable.just(Result.ok(message: "用戶名可用")) } return Observable.just(Result.failed(message: "用戶名不存在")) } // 登陸 func login(_ username:String, password:String) -> Observable<Result> { guard let userDict = NSDictionary(contentsOfFile: filePath), let userPass = userDict.object(forKey: username) else { return Observable.just(Result.empty) } if (userPass as! String) == password { return Observable.just(Result.ok(message: "登陸成功")) }else{ return Observable.just(Result.failed(message: "密碼錯誤")) } }
而後LoginViewModel.swift
,像這樣:
class LoginViewModel { let usernameUseable:Driver<Result> let loginButtonEnabled:Driver<Bool> let loginResult:Driver<Result> init(input:(username:Driver<String>, password:Driver<String>, loginTaps:Driver<Void>), service:ValidationService) { usernameUseable = input.username.flatMapLatest { userName in return service.loginUserNameValid(userName).asDriver(onErrorJustReturn: .failed(message: "鏈接server失敗")) } let usernameAndPass = Driver.combineLatest(input.username,input.password) { return ($0, $1) } loginResult = input.loginTaps.withLatestFrom(usernameAndPass).flatMapLatest{ (username, password) in service.login(username, password: password).asDriver(onErrorJustReturn: .failed(message: "鏈接server失敗")) } loginButtonEnabled = input.password.map { $0.characters.count > 0 }.asDriver() } }
Driver
類型的,第一個是username
處理結果流,第二個是登陸按鈕是否可用的流,第三個是登陸結果流;init
方法,看着和剛纔的註冊界面不同。這種寫法我參考了官方文檔的寫法,讓你們知道有這種寫法。可是我並不推薦你們使用這種方式,由於若是Controller
中的元素不少的話,一個一個傳過來是很可怕的。input
元組,包括username
的Driver
序列,password
的Driver
序列,還有登陸按鈕點擊的Driver
序列,還有Service
對象,須要Controller
傳遞過來,其實Controller
不該該擁有Service
對象。Driver
,咱們再也不須要shareReplay(1)
。接下來咱們在LoginViewController.swift
中寫,它看來像這樣子的:
override func viewDidLoad() { super.viewDidLoad() title = "登陸" let viewModel = LoginViewModel(input: (username: usernameTextField.rx.text.orEmpty.asDriver(), password: passwordTextField.rx.text.orEmpty.asDriver(), loginTaps:loginButton.rx.tap.asDriver()), service: ValidationService.instance) viewModel.usernameUseable.drive(nameLabel.rx.validationResult).disposed(by: disposeBag) viewModel.loginButtonEnabled.drive(onNext: { [weak self] (valid) in self?.loginButton.isEnabled = valid self?.loginButton.alpha = valid ? 1.0 : 0.5 }).disposed(by: disposeBag) viewModel.loginResult.drive(onNext: { [weak self](result) in switch result { case let .ok(message): self?.performSegue(withIdentifier: "showListSegue", sender: nil) self?.showAlert(message: message) case .empty: self?.showAlert(message: "") case let .failed(message): self?.showAlert(message: message) } }).disposed(by: disposeBag) }
viewModel
傳入相應的Driver序列。viewModel
中的對象進行相應的監聽,若是是Driver
序列,咱們這裏不使用bingTo
,而是使用的Driver
,用法和bingTo
如出一轍。Deriver
的監聽必定發生在主線程,因此很適合咱們更新UI的操做。因爲篇幅緣由,列表界面就不作很複雜了,簡單地弄了些假數據。既然作到這裏了,怎麼也得把它作完吧。
let's go,在storyboard
中添加一個控制器,佈局以下圖:
而後創建對應的ListViewController.swift
、ListViewModel.swift
文件,由於須要model
類,因此建立了一個Contact.swift
類,而後添加了contact.plist
資源文件。
首先編寫咱們的Contact.swift
類,它看來像這樣子:
class Contact:NSObject { var name:String var phone:String init(name:String, phone:String) { self.name = name self.phone = phone } }
而後在Service.swift
文件中,添加一個SearchService
類,它看起來像這樣:
class SearchService { static let instance = SearchService(); private init(){} // 獲取聯繫人 func getContacts() -> Observable<[Contact]> { let contactPath = Bundle.main.path(forResource: "Contact", ofType: "plist") let contactArr = NSArray(contentsOfFile: contactPath!) as! Array<[String:String]> var contacts = [Contact]() for contactDict in contactArr { let contact = Contact(name:contactDict["name"]!, phone: contactDict["phone"]!) contacts.append(contact) } return Observable.just(contacts).observeOn(MainScheduler.instance) } }
Contact
模型;Contact
數組的Observable
流。接下來更新UI的操做要在主線程中。而後看看咱們的ListViewModel.swift
,它看起來像這樣:
class ListViewModel { var models:Driver<[Contact]> init(with searchText:Observable<String>, service:SearchService){ models = searchText.debug() .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) .flatMap { text in return service.getContacts(withName: text) }.asDriver(onErrorJustReturn:[]) } }
models
是一個Driver
流,由於更新tableView
是UI操做;service
去獲取數據的操做應該在後臺線程去運行,因此添加了observeOn
操做;flatMap
返回新的observable
流,轉換成models
對應的Driver
流。注意:由於這裏是根據搜索框的內容去搜索數據,所以在SearchService
中須要添加一個函數,它看起來應該是這樣子的:
func getContacts(withName name: String) -> Observable<[Contact]> { if name == "" { return getContacts() } let contactPath = Bundle.main.path(forResource: "Contact", ofType: "plist") let contactArr = NSArray(contentsOfFile: contactPath!) as! Array<[String:String]> var contacts = [Contact]() for contactDict in contactArr { if contactDict["name"]!.contains(name) { let contact = Contact(name:contactDict["name"]!, phone: contactDict["phone"]!) contacts.append(contact) } } return Observable.just(contacts).observeOn(MainScheduler.instance) }
最後,咱們的ListViewController
就簡單了:
var searchBarText:Observable<String> { return searchBar.rx.text.orEmpty.throttle(0.3, scheduler: MainScheduler.instance) .distinctUntilChanged() } override func viewDidLoad() { super.viewDidLoad() title = "聯繫人" let viewModel = ListViewModel(with: searchBarText, service: SearchService.instance) viewModel.models.drive(tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)){(row, element, cell) in cell.textLabel?.text = element.name cell.detailTextLabel?.text = element.phone }.disposed(by: disposeBag) }
發現木有,這裏咱們麼有使用到DataSource
,將數據綁定到tableView
的items
元素,這是RxCocoa
對tableView
的一個擴展方法。咱們能夠點進去看看,一共有三個items
方法,而且文檔都有舉例,咱們使用的是
public func items<S : Sequence, Cell : UITableViewCell, O : ObservableType where O.E == S>(cellIdentifier: String, cellType: Cell.Type = default) -> (O) -> (@escaping (Int, S.Iterator.Element, Cell) -> Swift.Void) -> Disposable
這是一個柯里化的方法,不帶section
的時候使用這個,它有兩個參數,一個是循環利用的cell
的identifier
,一個cell
的類型。後面會返回的是一個閉包,在閉包裏對cell
進行設置。方法用起來比較簡單,就是有點難理解。
ok,到此爲止,此次實戰也算結束了。運行你的項目看看吧。
若是發現文章有錯誤的地方,歡迎指出,謝謝!!