(點擊上方公眾號,可快速關注)
來源:沸沸騰(@請叫我沸沸騰 )
www.jianshu.com/p/089ae5bececa
如有好文章投稿,請點擊 → 這裡了解詳情
前面兩篇關於RxSwift的文章都是一些概念,我估計大夥看了一定是迷迷糊糊的,還是不知道RxSwift怎麼使用,那麼這裡俺就帶領大夥一起去做一個Demo,去實戰一下RxSwift,大夥耐心寫完,理解透徹以後,保證大夥能掌握到RxSwift基本核心用法。
掌握了這篇內容,再回頭看下前面兩篇文章,保證你會豁然開朗某些概念,對RxSwift掌握的更加深入。這篇文章沒有對線程過分強調,請求都會在當前線程完成,計劃會在下篇文章中講解對線程的區別。
Demo
Demo地址是 github.com/CoderTian/LoginWithRxSwift 咱們這個Demo選用了最萬能的登錄註冊功能,先來看下Demo的一些基本演示,並未包含所有細節,
註冊界面
登錄界面
點擊輸入用戶名,如果本地plist文件中沒有註冊過這個用戶,會提示用戶名不存在
點擊輸入用戶名,如果本地plist文件中有註冊過這個用戶,會提示用戶名可用
輸入密碼點擊登錄,如果密碼錯誤提示密碼錯誤
輸入密碼點擊登錄,如果密碼正確則跳入列表界面,然後提示登錄成功
列表界面
好了差不多就先搞這麼些功能吧?什麼你居然認為內容很多?放心東西很簡單的,慢慢地參考著寫唄,每天搞定一個界面就中了,技術妥妥的提升!
用到的RxSwift概念
註冊界面,我們使用Observale, Variable, Subject,bingTo等
登錄界面,我們主要去使用Driver
列表界面,這個很簡單,TableView打算後面單獨寫一篇,主要是簡單的tableView展現和搜索
demo是使用的純MVVM模式,因為RxSwift就是為MVVM而生。不懂MVVM的童鞋請看MVVM模式快速入門 ,我默認大家對MVVM有大致的了解。
另外demo使用了carthage引入的RxSwift和RxCocoa,當然你也可以使用cocoapods引入這些東西。具體怎麼引入請大家看github介紹吧。
let’s go
首先請大家建立一個新的Swift項目,然後把RxSwift和RxCocoa引入到項目中。為什麼要引入RxCocoa?RxCocoa是對cocoa進行的Rx擴展,他已經包含了一個我們需要使用到的observable流,比如button的tap事件,已經幫我們包裝成了一個observable流。一般做iOS開發的要使用到RxSwift都要用到RxCocoa的,這兩個是相輔相成的。所有在所有的ViewController和ViewModel文件中引入這兩個文件
import RxCocoa
import RxSwift
登錄界面
這個界面我們主要學習使用Obserable和Subject的使用
大家先在storyboard上面建立好這個樣子的界面
建立需要文件
然後建立對應的RegisterViewController,他看起來應該是下面這樣子
class RegisterViewController: UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var passwordLabel: UILabel!
@IBOutlet weak var repeatPasswordTextField: UITextField!
@IBOutlet weak var repeatPasswordLabel: UILabel!
@IBOutlet weak var registerButton: UIButton!
@IBOutlet weak var loginButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
}
}
另外建立一個RegisterViewModel.swift文件,一個Protocol.swift文件,一個Service.swift文件
我們先寫那個比較好呢?我比較習慣先寫Service,我們就先寫Service吧,service文件主要負責一些網路請求,和一些數據的訪問操作。然後供ViewModel去使用。
首先我們在Service文件中創建ValidationService類,最好不要繼承NSObject,Swift中推薦儘量使用原生類。我們需要考慮當文本框裡面內容改變的時候,我們需要把傳來的username進行處理,判斷是否符合我們的條件,然後返回處理結果,也就是狀態。我們在protocol.swift文件中使用一個枚舉表示我們處理結果:所以我們在protocol.swift文件中添加下面內容
// 表示我們的一些請求的結果
enum Result {
case ok(message: String)
case empty
case failed(message: String)
}
username的處理
username的處理我加了一個已存在的判斷,效果如下
好了回到我們的ValidationService類中,我們寫處理username的方法。他應該看起來是下面這樣子。
class ValidationService {
static let instance = ValidationService()
private init() {}
let minCharactersCount = 6
//這裡面我們返回一個Observable對象,因為我們這個請求過程需要被監聽。
func validateUsername(_ username: String) -> Observable {
if username.characters.count == 0 {//當字符等於0的時候什麼都不做
return .just(.empty)
}
if username.characters.count Bool {
let filePath = NSHomeDirectory() + "/Documents/users.plist"
let userDic = NSDictionary(contentsOfFile: filePath)
let usernameArray = userDic!.allKeys as NSArray
if usernameArray.contains(username) {
return true
} else {
return false
}
}
}
下面開始寫我們的RegisterViewModel.swift,我們聲明一個username他是一個Variable的對象,為什麼是一個Variable對象呢?因為username既是一個observable也是一個observer,所以我們聲明為他為一個Variable對象。我們對username進行處理的應該有一個結,結果應該是需要界面去監聽來改變界面,因為處理的結果不需要是一個observer,所以我們把它聲明為一個Observable類型。所以你的RegisterViewModel類應該是這樣子。
class RegisterViewModel {
//input:
let username = Variable("") //初始值為""
// output:
let usernameUsable: Observable
init() {
}
}
然後我們在寫我們的RegisterViewController.swift文件,viewDidLoad()看起來應該是下面這個樣子
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = RegisterViewModel()
usernameTextField.rx.text.orEmpty
.bindTo(viewModel.username)
.addDisposableTo(disposeBag)
}
其中usernameTextField.rx.text.orEmpty是RxCocoa庫中的東西,他把TextFiled的text變成了一個Observable,後面的orEmpty我們可以Command點進去看下,他會把String?過濾nil幫我們變為String類型。
好了,因為我們的username既是一個observable也是一個observer,此時此刻我們把他當成一個Observer綁定到usernameTextFiled上,監聽我們的usernameTextField流。綁定就是監聽,bingTo裡面裡面的就是監聽者也就是Observer
因為我們有監聽,就要有監聽資源的回收,所以我們創建一個disposeBag來盛放我們的這些監聽資源。
好了回到我們的RegisterViewModel類中,我們添加下面的代碼
init() {
let service = ValidationService.instance
usernameUsable = username.asObservable()
.flatMapLatest{ username in
return service.validateUsername(username)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(.failed(message: "username檢測出錯"))
}
.shareReplay(1)
}
因為username是Variable類型,既可以當observer也可以當observable,viewModel中我們把它當成observable,然後對裡面的元素進行監聽和處理,這裡面我們使用了flatMap,因為我們需要返回一個新的序列,也就是返回處理結果,因為涉及到資料庫操作或者網絡請求(當然是模擬的網絡請求),所以這個序列需要我們去監聽,這種情況我們使用flatMap(具體請參考)
後面使用.shareReplay(1)是因為我們要保證無論多少個Observer來監聽我們這個序列,username的處理代碼我們只執行一次,這一次請求結果供多有的observer去使用。
下面我們在RegisterViewController中處理我們的username請求結果。我們在ViewDidLoad中添加下列代碼
viewModel.usernameUsable
.bindTo(usernameLabel.rx.validationResult)
.addDisposableTo(disposeBag)
viewModel.usernameUsable
.bindTo(passwordTextField.rx.inputEnabled)
.addDisposableTo(disposeBag)
上面的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 Reactive where Base: UILabel {
var validationResult: UIBindingObserver {
return UIBindingObserver(UIElement: base) { label, result in
label.textColor = result.textColor
label.text = result.description
}
}
}
extension Reactive where Base: UITextField {
var inputEnabled: UIBindingObserver {
return UIBindingObserver(UIElement: base) { textFiled, result in
textFiled.isEnabled = result.isValid
}
}
}
我們首先對Result進行了擴展,添加了一個isValid屬性,如果他的狀態是ok,這個屬性就返回true,否則就返回false
然後對Result添加了一個textColor屬性,如果Result屬性為ok的時候顏色就是綠色,否則即使紅色
下面我們自定義了一個Observer,對UIlabel進行了擴展,根據result結果,進行他的text和textColor的顯示
最後我們對UITextField進行擴展,根據result結果,進行他的isEnabled進行設置
好了到了這裡我們就可以運行項目來看下程序的運行情況,試著去輸入username嘗試一下吧。激動了不?
總結一下這個過程:
輸入文本框Observable流->ViewModel中username對文本框進行監聽->然後username調用service進行處理得到usernameUsable結果流->提示lable對usernameUsable進行監聽刷新UI。
其實就是兩個流的過程
UI操作->ViewModel->改變數據
數據改變->ViewModel->UI刷新
哈哈,這就是響應式編程,看起來是不是一路的監聽啊
password的處理
有了上面的理解,對password的處理我們就輕車熟路了。很簡單了,有些概念我就不需要解釋太多了。
我們現在Service中添加對password的處理方法
func validatePassword(_ password: String) -> Result {
if password.characters.count == 0 {
return .empty
}
if password.characters.count Result {
if repeatedPasswordword.characters.count == 0 {
return .empty
}
if repeatedPasswordword == password {
return .ok(message: "密碼可用")
}
return .failed(message: "兩次密碼不一樣")
}
validatePassword處理我們輸入的password
validateRepeatedPassword處理我們的密碼確認
上面的返回結果都是一個Result類型的值,因為我們外面不要對這個處理過程進行監聽,所以不必返回一個新的序列
然後我們在RegisterViewModel.swift文件中,添加需要的observale
//input:
let password = Variable("")
let repeatPassword = Variable("")
//output:
let passwordUsable: Observable //密碼是否可用
let repeatPasswordUsable: Observable //密碼確定是否正確
然後我們在RegisterViewController中,添加passwordTextField綁定
passwordTextField.rx.text.orEmpty
.bindTo(viewModel.password)
.addDisposableTo(disposeBag)
repeatPasswordTextField.rx.text.orEmpty
.bindTo(viewModel.repeatPassword)
.addDisposableTo(disposeBag)
然後再轉移到RegisterViewModel中,處理我們的password的輸入
passwordUsable = password.asObservable()
.map { password in
return service.validatePassword(password)
}
.shareReplay(1)
repeatPasswordUsable = Observable.combineLatest(password.asObservable(), repeatPassword.asObservable()) {
return service.validateRepeatedPassword($0, repeatedPasswordword: $1)
}
.shareReplay(1)
下面轉移到RegisterViewController中對ViewModel中的output進行處理
viewModel.passwordUsable
.bindTo(passwordLabel.rx.validationResult)
.addDisposableTo(disposeBag)
viewModel.passwordUsable
.bindTo(repeatPasswordTextField.rx.inputEnabled)
.addDisposableTo(disposeBag)
viewModel.repeatPasswordUsable
.bindTo(repeatPasswordLabel.rx.validationResult)
.addDisposableTo(disposeBag)
passwordLabel對viewModel.passwordUsable進行監聽,顯示不同的文案提示
repeatPasswordTextField對passwordUsable進行監聽,結果ok可輸入狀態,否則就是不可輸入
repeatPasswordTextField對repeatPasswordUsable進行監聽,顯示不同的文案提示
呼呼!好了運行下程序看看吧,輸入password和確定密碼感受下吧。
彈框方法
func showAlert(message: String) {
let action = UIAlertAction(title: "確定", style: .default, handler: nil)
let alertViewController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertViewController.addAction(action)
present(alertViewController, animated: true, completion: nil)
}
列表界面
下面是列表界面,限於篇幅的原因,這裡我只寫展現,具體的搜索功能和其他tableView的展現技術,我會在下篇文章中進行分享。
在StoryBoard文件中新建列表界面
然後建立相應的ContainerViewController.swift和ContainerViewModel.swift文件。和Hero.swift文件。將所需的圖片資源導入,下載Demo,Demo中有。
首先編寫我們Hero類
class Hero: NSObject {
var name: String
var desc: String
var icon: String
init(name: String, desc: String, icon: String) {
self.name = name
self.desc = desc
self.icon = icon
}
}
然後我們在Service文件中添加一個新的Service類,或者在原來的類中添加方法
class SearchService {
static let shareInstance = SearchService()
private init() {}
func getHeros() -> Observable {
let herosString = Bundle.main.path(forResource: "heros", ofType: "plist")
let herosArray = NSArray(contentsOfFile: herosString!) as! Array
var heros = [Hero]()
for heroDic in herosArray {
let hero = Hero(name: heroDic["name"]!, desc: heroDic["intro"]!, icon: heroDic["icon"]!)
heros.append(hero)
}
return Observable.just(heros)
.observeOn(MainScheduler.instance)
}
}
然後看下我們的ContainerViewModel
class ContainerViewModel {
// output:
var models: Driver
init(withSearchText searchText: Observable, service: SearchService) {
models = searchText
.debug()
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.flatMap { text in
return service.getHeros(withName: text)
}.asDriver(onErrorJustReturn: [])
}
}
我們的output是一個Driver流,因為更新tableView是UI操作
然後我們使用service拉去數據的操作應該是在後臺線程去運行,所以添加了observeOn操作
然後使用flatMap返回新的Observable流,轉換成output的Driver流
我們的ContainerViewController類就很簡單了
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ContainerViewModel(withSearchText: searchBarText, service: SearchService.shareInstance)
viewModel.models
.drive(tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = element.name
cell.detailTextLabel?.text = element.desc
cell.imageView?.image = UIImage(named: element.icon)
}
.addDisposableTo(disposeBag)
}
我們點進去可以看下,一共有三個items方法,並且上面都有些使用方法,我們使用的這個是
public func items<s>(cellIdentifier: String, cellType: Cell.Type = default) -> (O) -> (@escaping (Int, S.Iterator.Element, Cell) -> Swift.Void) -> Disposable</s>
這是一個科裡化的方法,不帶section的時候使用這個,他有兩個參數,一個是循環利用的cell的identifier,一個cell的類型。後面會返回的是一個閉包,在閉包裡對cell進行設置。方法用起來比較簡單,就是有點難理解。
好了現在運行你的代碼吧,騷年開始在RxSwift的海洋中自由式吧
覺得本文對你有幫助?請分享給更多人
關注「 iOS大全 」
看更多精選 iOS 技術文章