Function Reactive Programming:函數(shù)響應(yīng)式編程是種編程范式。我們通過(guò)不同的構(gòu)建函數(shù),來(lái)創(chuàng)建所需要的數(shù)據(jù)序列。最后通過(guò)適當(dāng)?shù)姆绞絹?lái)響應(yīng)這個(gè)序列。這就是函數(shù)響應(yīng)式編程。它結(jié)合了函數(shù)式編程以及響應(yīng)式編程。
函數(shù)式編程
Masonry、SnapKit 就是我們最常見(jiàn)的函數(shù)式編程,通過(guò)對(duì)象.方法1().方法2.....
在程序開發(fā)中:
a = b + c賦值之后 b 或者 c 的值變化后,a 的值不會(huì)跟著變化
響應(yīng)式編程
響應(yīng)式編程,目標(biāo)就是,如果 b 或者 c 的數(shù)值發(fā)生變化,a 的數(shù)值會(huì)同時(shí)發(fā)生變化;
RxSwift 核心思想就是 FRP。
一、RxSwift 是什么?
先說(shuō)下 Rx:
ReactiveX,簡(jiǎn)寫 Rx,是一個(gè)可以幫助我們簡(jiǎn)化異步編程的框架,它對(duì)觀察者模式進(jìn)行了擴(kuò)展,讓我們可以自由組合多個(gè)事件。(Rx 支持幾乎全部的流行編程語(yǔ)言,有 RxJava/RxJS/Rx.NET 等)
社區(qū)網(wǎng)站: reactivex.io
然后再說(shuō)下 RxSwift :
RxSwift 是 Rx 的 Swift 版本,它嘗試將原有的一些概念移植到 iOS/macOS 平臺(tái)。
RxSwift 將程序中的事件傳遞相應(yīng)方法進(jìn)行了統(tǒng)一,將其(delegate、notification、target-action 等)全替換成 Rx 的“信號(hào)鏈”方式
基本原理:
用戶輸入、點(diǎn)擊事件、定時(shí)器、網(wǎng)絡(luò)請(qǐng)求等都可以當(dāng)成 Observable(被觀察者),Observer(觀察者)總會(huì)在 Observable 處注冊(cè)一個(gè)訂閱,當(dāng)事件發(fā)生時(shí),Observable 找到所有的訂閱并通知觀察者。
二、為什么要使用 RxSwift
復(fù)合 - Rx 就是復(fù)合的代名詞
復(fù)用 - 因?yàn)樗讖?fù)合
清晰 - 因?yàn)槁暶鞫际遣豢勺兏?/p>
易用 - 因?yàn)樗橄蟮牧水惒骄幊蹋刮覀兘y(tǒng)一了代碼風(fēng)格
穩(wěn)定 - 因?yàn)?Rx 是完全通過(guò)單元測(cè)試的
具體來(lái)說(shuō)就是
在編寫代碼時(shí)我們經(jīng)常會(huì)需要檢測(cè)某些值的變化,然后進(jìn)行相應(yīng)的處理。比如說(shuō)按鈕的點(diǎn)擊事件、textFiled 值的變化、tableView 中 cell 的點(diǎn)擊事件等等。在日常搬磚中,我們針對(duì)不同的情況,需要采用不同的事件傳遞方法去處理,比如 delegate、Notifinotion、Target Action、KVO 等等。
三、如何使用 RxSwift
引入的兩個(gè)庫(kù)
import RxSwift
import RxCocoa
- RxSwift:它只是基于 Swift 語(yǔ)言的 Rx 標(biāo)準(zhǔn)實(shí)現(xiàn)接口庫(kù),所以 RxSwift 里不包含任何 Cocoa 或者 UI 方面的類。
- RxCocoa:是基于 RxSwift 針對(duì)于 iOS 開發(fā)的一個(gè)庫(kù),它通過(guò) Extension 的方法給原生的比如 UI 控件添加了 Rx 的特性,使得我們更容易訂閱和響應(yīng)這些控件的事件。
看下面幾個(gè)例子:
1. Target Action
傳統(tǒng)方式實(shí)現(xiàn)
button.addTarget(self, action: #selector(buttonEvent), for: .touchUpInside)
@objc func buttonEvent() {
print("button Event")
}
用 RxSwift 實(shí)現(xiàn)
button.rx.tap
.subscribe(onNext: {
print("button event")
})
.disposed(by: disposeBag)
2. Notifinotion
傳統(tǒng)方式實(shí)現(xiàn)
NotificationCenter.default.addObserver(self, selector: #selector(notificationAction(_:)), name: NSNotification.Name("NotificationName"), object: nil)
@objc func notificationAction(_ notification: Notification) {
// do something
}
用 RxSwift 實(shí)現(xiàn)
NotificationCenter.default.rx.notification(NSNotification.Name("NotificationName"))
.subscribe(onNext: { (notification) in
// do something
})
.disposed(by: disposeBag)
3. delegate
傳統(tǒng)方式實(shí)現(xiàn)
// 遵循代理
class ViewController: UITextFieldDelegate
// 設(shè)置代理
textField.delegate = self
// 實(shí)現(xiàn)代理方法
func textFieldDidEndEditing(_ textField: UITextField) {
// do something
}
用 RxSwift 實(shí)現(xiàn)
textField.rx.controlEvent([.editingDidEnd])
.asObservable()
.subscribe(onNext: { _ in
// do something
})
.disposed(by: disposeBag)
// 字符輸入時(shí)
textField.rx.controlEvent([.editingChanged])
.asObservable()
.subscribe(onNext: { _ in
print("字符輸入:\(self.textField.text ?? "")")
})
.disposed(by: disposeBag)
// 或者
textField.rx.text
.subscribe(onNext: { text in
print("text:\(text ?? "")")
})
.disposed(by: disposeBag)
RxCocoa 用 extensiton 的方式,為 UITextfield,UIlabel 等控件添加了很多可監(jiān)聽的屬性,這里的 textfield.rx.text 就是一個(gè)
看上面幾個(gè)例子,和傳統(tǒng)方式相比,Rx 的代碼更加清晰簡(jiǎn)潔,易讀、易維護(hù)。
4. BehaviorRelay
作用:存儲(chǔ)時(shí)常需要更新的數(shù)據(jù)
let number: BehaviorRelay = BehaviorRelay<Int>(value: 0)
let disposeBag = DisposeBag()
// 訂閱 number 的變化,skip(1): 跳過(guò)初始化時(shí)值的變化
number.skip(1).subscribe(onNext: { (num) in
print("數(shù)字變化:\(num)")
}).disposed(by: disposeBag)
for i in 1...5 {
number.accept(i)
}
輸出結(jié)果
數(shù)字變化:1
數(shù)字變化:2
數(shù)字變化:3
數(shù)字變化:4
數(shù)字變化:5
5. 綁定
let isCodeValid = textField.rx.text
.orEmpty
.map { $0.count == 6 }
.share(replay: 1)
// 驗(yàn)證碼輸入是否有效 -> 驗(yàn)證碼提示語(yǔ)是否隱藏
isCodeValid
.bind(to: errorLabel.rx.isHidden)
.disposed(by: disposeBag)
// 驗(yàn)證碼輸入是否有效 -> 提交按鈕是否可點(diǎn)擊
isCodeValid
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
代碼的簡(jiǎn)單說(shuō)明:
- orEmpty:可以將 String? 類型的 ControlProperty(ControlProperty 說(shuō)明是被觀察者) 轉(zhuǎn)成 String,省得我們?cè)偃ソ獍#ㄒ?jiàn)下圖)
- share(replay: 1) :我們用 isCodeValid 來(lái)控制驗(yàn)證碼提示語(yǔ)是否隱藏以及登錄按鈕是否可用。shareReplay 就是讓他們共享這一個(gè)源,而不是為他們單獨(dú)創(chuàng)建新的源。這樣可以減少不必要的開支。
- DisposeBag:作用是 Rx 在視圖控制器或者其持有者將要銷毀的時(shí)候,自動(dòng)釋法掉綁定在它上面的資源。它是通過(guò)類似“訂閱處置機(jī)制”方式實(shí)現(xiàn)(類似于 NotificationCenter 的 removeObserver)。
從下圖的源碼中我們可以看到 UITextField 的 rx.text 屬性類型便是 ControlProperty<String?>
6. TableView
傳統(tǒng)方式實(shí)現(xiàn)
import Foundation
// 聯(lián)系人列表數(shù)據(jù)源
struct ContactsViewModel {
let data = [
Contacts("HarrySun1", phoneNumber: "1"),
Contacts("HarrySun2", phoneNumber: "12"),
Contacts("HarrySun3", phoneNumber: "123"),
Contacts("HarrySun4", phoneNumber: "1234"),
Contacts("HarrySun5", phoneNumber: "12345")
]
}
/*
接著我們?cè)O(shè)置 UITableView 的委托,并讓視圖控制器實(shí)現(xiàn) UITableViewDataSource 和 UITableViewDelegate 協(xié)議及相關(guān)的協(xié)議方法。
這個(gè)大家肯定都寫過(guò)無(wú)數(shù)遍了,也沒(méi)什么好講的
*/
import UIKit
import RxSwift
class ViewController: UIViewController {
//tableView對(duì)象
@IBOutlet weak var tableView: UITableView!
// 聯(lián)系人列表數(shù)據(jù)源
let contactsViewModel = ContactsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
//設(shè)置代理
tableView.dataSource = self
tableView.delegate = self
}
}
extension ViewController: UITableViewDataSource {
//返回單元格數(shù)量
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contactsViewModel.data.count
}
//返回對(duì)應(yīng)的單元格
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
let contacts = contactsViewModel.data[indexPath.row]
cell.textLabel?.text = "\(contacts.name) : \(contacts.phoneNumber)"
return cell
}
}
extension ViewController: UITableViewDelegate {
//單元格點(diǎn)擊
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let contacts = contactsViewModel.data[indexPath.row]
print("你選中的聯(lián)系人信息【\(contacts)】")
}
}
用 RxSwift 實(shí)現(xiàn)
// 對(duì) viewModel 修改
/*
這里我們將 data 屬性變成一個(gè)可觀察序列對(duì)象(Observable Squence),而對(duì)象當(dāng)中的內(nèi)容和我們之前在數(shù)組當(dāng)中所包含的內(nèi)容是完全一樣的。
簡(jiǎn)單說(shuō)就是“序列”可以對(duì)這些數(shù)值進(jìn)行“訂閱(Subscribe)”,有點(diǎn)類似于“通知(NotificationCenter)”
*/
import RxSwift
// 聯(lián)系人列表數(shù)據(jù)源
struct ContactsViewModel {
// 創(chuàng)建發(fā)送指定值的 Observerble
let data = Observable.just([
Contacts("HarrySun1", phoneNumber: "1"),
Contacts("HarrySun2", phoneNumber: "12"),
Contacts("HarrySun3", phoneNumber: "123"),
Contacts("HarrySun4", phoneNumber: "1234"),
Contacts("HarrySun5", phoneNumber: "12345")
])
}
// 對(duì)視圖控制器代碼修改
/*
這里我們不再需要實(shí)現(xiàn)數(shù)據(jù)源和委托協(xié)議了。而是寫一些響應(yīng)式代碼,讓它們將數(shù)據(jù)和 UITableView 建立綁定關(guān)系。
*/
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
let tableView = UITableView()
// 聯(lián)系人列表數(shù)據(jù)源
let contactsViewModel = ContactsViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
//將數(shù)據(jù)源數(shù)據(jù)綁定到tableView上
contactsViewModel.data
.bind(to: tableView.rx.items(cellIdentifier:"Cell")) { _, contacts, cell in
cell.textLabel?.text = "\(contacts.name) : \(contacts.phoneNumber)"
}.disposed(by: disposeBag)
//tableView點(diǎn)擊響應(yīng)
tableView.rx.modelSelected(Contacts.self).subscribe(onNext: { contacts in
print("你選中的聯(lián)系人信息【\(contacts)】")
}).disposed(by: disposeBag)
}
}
代碼的簡(jiǎn)單說(shuō)明:
- rx.items(cellIdentifier:):這是 Rx 基于 cellForRowAt 數(shù)據(jù)源方法的一個(gè)封裝。傳統(tǒng)方式中我們還要有個(gè) numberOfRowsInSection 方法,使用 Rx 后就不再需要了(Rx 已經(jīng)幫我們完成了相關(guān)工作)。
- rx.modelSelected: 這是 Rx 基于 UITableView 委托回調(diào)方法 didSelectRowAt 的一個(gè)封裝。
更多例子參見(jiàn) RxSwift 中文文檔:為什么要使用 RxSwift ?
上面看了幾個(gè) RxSwift 的例子,也感受到了函數(shù)響應(yīng)式編程 清晰簡(jiǎn)潔、易讀、易維護(hù)的代碼,下面我們看下 RxSwift 的最佳搭檔:MVVM
MVVM
原先常用的架構(gòu):MVC
- Model:數(shù)據(jù)層。負(fù)責(zé)讀寫數(shù)據(jù),保存 App 狀態(tài)等
- Controller:業(yè)務(wù)邏輯層。負(fù)責(zé)業(yè)務(wù)邏輯、事件響應(yīng)、數(shù)據(jù)加工等工作
缺點(diǎn):
- ViewController 既扮演了 View 的角色,又扮演了 ViewController 的角色
- 而 Model 在 VIewController 中又可以直接與 View 進(jìn)行交互
- 當(dāng) App 交互復(fù)雜的時(shí)候,就會(huì)發(fā)現(xiàn) ViewController 將變得十分臃腫,大量代碼被添加到控制器中,使得控制器負(fù)擔(dān)過(guò)重。
MVVM
優(yōu)點(diǎn):
- 可以對(duì) ViewController 進(jìn)行瘦身
- 實(shí)現(xiàn)邏輯視圖的復(fù)用。比如一個(gè) ViewModel 可以綁定到不同的 View 上,讓多個(gè) View 重用相同的視圖邏輯。
- 而且使用 MVVM 可以大大降低代碼的耦合性,方便進(jìn)行單元測(cè)試以及維護(hù),也方便多人協(xié)作開發(fā)(比如一個(gè)人負(fù)責(zé)邏輯實(shí)現(xiàn),一個(gè)人負(fù)責(zé) UI 實(shí)現(xiàn))。
缺點(diǎn):
- 相較于 MVC,使用 MVVM 會(huì)輕微的增加代碼量,但是總體上減少了代碼的復(fù)雜性。
- 還有就是有一定的學(xué)習(xí)成本(如何數(shù)據(jù)綁定等)。
RxSwift 和 MVVM 結(jié)合使用
例1. 上面 tableView 的實(shí)現(xiàn)
例2. 兩個(gè)網(wǎng)絡(luò)請(qǐng)求返回?cái)?shù)據(jù)后跳轉(zhuǎn)
// endAction 和 payAction 是兩個(gè)網(wǎng)絡(luò)請(qǐng)求,把兩個(gè)接口請(qǐng)求是否完成壓縮為一個(gè)信號(hào)再做操作
Observable.merge(viewModel.endAction.elements, viewModel.payAction.elements)
.observeOn(MainScheduler.instance) // 保證在主線程
.subscribe(onNext: { [weak self] model in
// doSomething
})
.disposed(by: disposeBag)
代碼簡(jiǎn)單說(shuō)明
merge
將多個(gè) Observables 合并成一個(gè)
observeOn
指定 Observable 在那個(gè) Scheduler 發(fā)出通知
Action
Action 被定義為一個(gè)類 Action<Input, Element>,Input 是輸入的元素,Element 是 Action 處理完之后返回的元素。
一般綁定到 button 上:button.rx.action = action
action.execute():執(zhí)行 Action
action.elements:請(qǐng)求成功,可以拿到 Action 返回的 Element
action.executing:請(qǐng)求是否在執(zhí)行,可以用來(lái)展示或者隱藏 HUD
詳細(xì)的可以看附件代碼 - 4:使用 Action 寫一個(gè)網(wǎng)絡(luò)請(qǐng)求
Demo
demo 可以在 GitHub 倉(cāng)庫(kù) 找到
附件代碼
1. ViewController.swift
//
// ViewController.swift
// RxSwiftTest
//
// Created by 孫浩 on 2019/7/26.
// Copyright ? 2019 HarrySun. All rights reserved.
//
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
let errorLabel = UILabel()
let button = UIButton()
let textField = UITextField()
let number: BehaviorRelay = BehaviorRelay<Int>(value: 0)
let tableView = UITableView()
// 聯(lián)系人列表數(shù)據(jù)源
let contactsViewModel = ContactsViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// rxButton()
// rxNotification()
rxDelegate()
// rcTableView()
// rxBinding()
}
// 測(cè)試 button
func rxButton() {
view.addSubview(button)
button.frame = CGRect(x: 100, y: 100, width: 100, height: 50)
button.backgroundColor = .red
button.rx.tap
.subscribe(onNext: {
print("button event")
NotificationCenter.default.post(name: NSNotification.Name("NotificationName"), object: nil)
})
.disposed(by: disposeBag)
}
// 測(cè)試 Notification
func rxNotification() {
NotificationCenter.default.rx.notification(NSNotification.Name("NotificationName"))
.subscribe(onNext: { (notification) in
print("notification")
})
.disposed(by: disposeBag)
}
// 測(cè)試 delegate
func rxDelegate() {
view.addSubview(textField)
textField.layer.borderWidth = 1
textField.frame = CGRect(x: 10, y: 100, width: 300, height: 50)
textField.rx.controlEvent([.editingChanged])
.asObservable()
.subscribe(onNext: { _ in
print("字符輸入:\(self.textField.text ?? "")")
})
.disposed(by: disposeBag)
// 或者
textField.rx.text
.subscribe(onNext: { text in
print("text:\(text ?? "")")
})
.disposed(by: disposeBag)
textField.rx.controlEvent([.editingDidEnd])
.asObservable()
.subscribe(onNext: { _ in
// do something
})
.disposed(by: disposeBag)
}
// 測(cè)試 BehaviorRelay
func rcBehaviorRelay() {
number.skip(1).subscribe(onNext: { (num) in
print("數(shù)字變化:\(num)")
}).disposed(by: disposeBag)
for i in 1...5 {
number.accept(i)
}
}
// 測(cè)試數(shù)據(jù)綁定
func rxBinding() {
let isCodeValid = textField.rx.text
.orEmpty
.map { $0.count == 6 }
.share(replay: 1)
// share(replay: 1) 是用來(lái)做什么的?
// 我們用 isCodeValid 來(lái)控制驗(yàn)證碼提示語(yǔ)是否隱藏以及登錄按鈕是否可用。shareReplay 就是讓他們共享這一個(gè)源,而不是為他們單獨(dú)創(chuàng)建新的源。這樣可以減少不必要的開支。
// 驗(yàn)證碼輸入是否有效 -> 提交按鈕是否可點(diǎn)擊
isCodeValid
.bind(to: errorLabel.rx.isHidden)
.disposed(by: disposeBag)
isCodeValid
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
}
// 測(cè)試 tableView
func rcTableView() {
view.addSubview(tableView)
tableView.frame = view.frame
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
//將數(shù)據(jù)源數(shù)據(jù)綁定到tableView上
contactsViewModel.data
.bind(to: tableView.rx.items(cellIdentifier:"Cell")) { _, contacts, cell in
cell.textLabel?.text = "\(contacts.name) : \(contacts.phoneNumber)"
}.disposed(by: disposeBag)
//tableView點(diǎn)擊響應(yīng)
tableView.rx.modelSelected(Contacts.self).subscribe(onNext: { contacts in
print("你選中的聯(lián)系人信息【\(contacts)】")
}).disposed(by: disposeBag)
}
}
2. ContactsViewModel.swift
//
// ContactsViewModel.swift
// RxSwiftTest
//
// Created by 孫浩 on 2019/7/26.
// Copyright ? 2019 HarrySun. All rights reserved.
//
import RxSwift
struct ContactsViewModel {
// 創(chuàng)建發(fā)送指定值的 Observerble
let data = Observable.just([
Contacts("HarrySun1", phoneNumber: "1"),
Contacts("HarrySun2", phoneNumber: "12"),
Contacts("HarrySun3", phoneNumber: "123"),
Contacts("HarrySun4", phoneNumber: "1234"),
Contacts("HarrySun5", phoneNumber: "12345")
])
}
3. Contacts.swift
//
// Contacts.swift
// RxSwiftTest
//
// Created by 孫浩 on 2019/7/26.
// Copyright ? 2019 HarrySun. All rights reserved.
//
import UIKit
class Contacts: NSObject {
var name = ""
var phoneNumber = ""
init(_ name: String, phoneNumber: String) {
self.name = name
self.phoneNumber = phoneNumber
}
}
4. 使用 Action 寫一個(gè)網(wǎng)絡(luò)請(qǐng)求
1.在 viewModel 中寫一個(gè)請(qǐng)求數(shù)據(jù)的 Action
lazy var shareAction = Action<Void, (URL?, String, String)> { // Action<input,output> { }
guard let orderno = self.endModel?.orderno, let location = LocationService.shared.lastLocation else {
return Observable.empty()
}
// 網(wǎng)絡(luò)請(qǐng)求
return ShareRequest(orderid: orderno, location: location)
.rx
.start(usingCache: true)
.mapModel(ShareModel.self)
.catchErrorJustReturn(self.defaultShareModel) // 請(qǐng)求接口失敗后取默認(rèn)的model
.map { // map:遍歷并做相應(yīng)處理
(URL(string: $0.shareUrl), $0.comment, " " + $0.hashtag + " ")
}
}
- 在 viewController 中調(diào)用一次 action.execute()
viewModel.shareAction.execute(())
- action中的請(qǐng)求成功,返回?cái)?shù)據(jù)之后的操作
viewModel.shareAction.elements
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] tuple in
guard let `self` = self else { return }
// 處理請(qǐng)求完成后的操作
})
.disposed(by: disposeBag)
- 請(qǐng)求中的操作
請(qǐng)求開始時(shí)顯示 HUD,請(qǐng)求結(jié)束后隱藏 HUD
distinctUntilChanged:值修改的時(shí)候才會(huì)走此方法
viewModel.shareAction.executing
.distinctUntilChanged() // 數(shù)據(jù)發(fā)生改變才會(huì)走
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] executing in
if executing {
ProgressHUD.showLoadingHUD(in: self?.view)
} else {
ProgressHUD.hideLoadingHUD(in: self?.view)
}
})
.disposed(by: disposeBag)