目錄
一、首先我們看看RxCocoa做了啥
1、UIView
2、UILabel
3、UIImageView
4、UIButton
5、UITextField
6、UIScrollView
7、UITableView
8、UICollectionView
二、然後我們寫個MVVM的小案例
1、不使用RxSwift時的MVVM
2、使用RxSwift時的MVVM
一、首先我們看看RxCocoa做了啥
爲了幫助我們更加簡單優雅地實現ViewModel和View的雙向綁定,RxCocoa已經幫我們把UIKit框架裏常用控件的常用屬性都搞成了Observable或Binder、有的屬性甚至是Subjects,這樣有的屬性就可以發出事件(以便讓數據監聽),有的屬性就可以監聽Observable——即數據,有的屬性既可以發出事件、也可以監聽Observable,因此在實際開發中UI這邊兒直接拿現成的用就行了,通常情況下我們只需要把數據定義成Subjects——這樣數據就可以發出事件(以便讓UI監聽)、也可以監聽Observable——即UI。
1、UIView
UIView的rx.backgroundColor
、rx.alpha
、rx.isHidden
、rx.isUserInteractionEnabled
屬性都是Binder,所以它們可以監聽Observable。
-
rx.backgroundColor
屬性
rx.backgroundColor
屬性是對傳統方式view.setBackgroundColor(...)
方法的封裝,我們可以用它來設置view的背景顏色。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var customView: UIView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置customView的背景顏色
//
// 1、observable負責發出事件,事件上掛的數據就是一個顏色
// 2、customView的rx.backgroundColor屬性就是一個binder,所以它可以監聽observable,當它收到observable發出的事件時,就會把事件上掛的顏色拿下來真正賦值給customView的backgroundColor屬性。還記得我們自己是怎麼創建Binder的吧,可以翻回去看一下,RxCocoa底層就是那麼實現的
let observable = Observable.just(UIColor.red)
let binder = customView.rx.backgroundColor
observable.bind(to: binder).disposed(by: bag)
}
}
-
rx.alpha
屬性
rx.alpha
屬性是對傳統方式view.setAlpha(...)
方法的封裝,我們可以用它來設置view的透明度。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var customView: UIView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置customView的透明度
Observable.just(0.618)
.bind(to: customView.rx.alpha)
.disposed(by: bag)
}
}
-
rx.isHidden
屬性
rx.isHidden
屬性是對傳統方式view.setHidden(...)
方法的封裝,我們可以用它來設置view是否隱藏。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var customView: UIView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置customView是否隱藏
Observable.just(true)
.bind(to: customView.rx.isHidden)
.disposed(by: bag)
}
}
-
rx.isUserInteractionEnabled
屬性
rx.isUserInteractionEnabled
屬性是對傳統方式view.setUserInteractionEnabled(...)
方法的封裝,我們可以用它來設置view是否能夠處理用戶交互。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var customView: UIView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置customView是否能夠處理用戶交互
Observable.just(false)
.bind(to: customView.rx.isUserInteractionEnabled)
.disposed(by: bag)
}
}
2、UILabel
UILabel的rx.text
、rx.attributedText
屬性都是Binder,所以它們可以監聽Observable。
-
rx.text
屬性
rx.text
屬性是對傳統方式label.setText(...)
方法的封裝,我們可以用它來設置label的文本。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置label的文本
Observable.just("Hello RxSwift")
.bind(to: label.rx.text)
.disposed(by: bag)
}
}
-
rx.attributedText
屬性
rx.attributedText
屬性是對傳統方式label.setAttributedText(...)
方法的封裝,我們可以用它來設置label的屬性文本。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置label的屬性文本
Observable.just("Hello RxSwift")
.map({ element in
let attributedString = NSAttributedString(string: element, attributes: [
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
NSAttributedString.Key.underlineColor: UIColor.red,
])
return attributedString
})
.bind(to: label.rx.attributedText)
.disposed(by: bag)
}
}
3、UIImageView
UIImageView的rx.image
屬性是Binder,所以它可以監聽Observable。
-
rx.image
屬性
rx.image
屬性是對傳統方式imageView.setImage(...)
方法的封裝,我們可以用它來設置imageView的圖片。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置imageView的圖片
Observable<Int>.timer(.seconds(0), period: .milliseconds(167), scheduler: MainScheduler.instance)
.map({ element in
let imageName = "idle_\(element)" // 映射出圖片名稱
let image = UIImage(named: imageName)!
return image
})
.bind(to: imageView.rx.image)
.disposed(by: bag)
}
}
4、UIButton
UIButton的rx.tap
屬性是Observable,所以它可以發出事件。
UIButton的rx.isEnabled
、rx.isSelected
屬性都是Observer,所以它們可以監聽Observable。
UIButton的rx.controlEvent(...)
方法的返回值是Observable,所以它可以發出事件。
UIButton的rx.title(for: ...)
、rx.image(for: ...)
、rx.backgroundImage(for: ...)
方法的返回值都是Binder,所以它們的返回值可以監聽Observable。
-
rx.tap
屬性
rx.tap
屬性是對傳統方式button.addTarget(..., action: #selector(...), for: .touchUpInside)
方法的封裝,我們可以用它來給button添加touchUpInside狀態下的點擊事件。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給button添加touchUpInside狀態下的點擊事件
button.rx.tap
.subscribe { _ in
print("按鈕被點擊了")
}
.disposed(by: bag)
}
}
-
rx.isEnabled
屬性
rx.isEnabled
屬性是對傳統方式button.setEnabled(...)
方法的封裝,我們可以用它來設置button是否可以點擊。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置button是否可以點擊
Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.map({ element in
let value = element % 2 == 0 // 映射爲bool值
return value
})
.bind(to: button.rx.isEnabled)
.disposed(by: bag)
}
}
-
rx.isSelected
屬性
rx.isSelected
屬性是對傳統方式button.setSelected(...)
方法的封裝,我們可以用它來設置button是否處於選中狀態。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置button是否可以點擊
Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.map({ element in
let value = element % 2 == 0 // 映射爲bool值
return value
})
.bind(to: button.rx.isSelected)
.disposed(by: bag)
}
}
-
rx.controlEvent(...)
方法
rx.controlEvent(...)
方法是對傳統方式button.addTarget(..., action: ..., for: ...)
方法的封裝,我們可以用它來給button添加任意狀態下的點擊事件,上面的rx.tap
屬性底層就是通過這個方法實現的,只不過鎖死了touchUpInside狀態。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給button添加任意狀態下的點擊事件
button.rx.controlEvent(.touchDown)
.subscribe { _ in
print("touchDown")
}
.disposed(by: bag)
}
}
-
rx.title(for: ...)
方法
rx.title(for: ...)
方法是對傳統方式button.setTitle(..., for: ...)
方法的封裝,我們可以用它來給button設置不同狀態下的文本。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給button設置不同狀態下的文本
Observable.just("正常狀態下的文本")
.subscribe(button.rx.title(for: .normal))
.disposed(by: bag)
Observable.just("高亮狀態下的文本")
.subscribe(button.rx.title(for: .highlighted))
.disposed(by: bag)
}
}
-
rx.image(for: ...)
方法
rx.image(for: ...)
方法是對傳統方式button.setImage(..., for: ...)
方法的封裝,我們可以用它來給button設置不同狀態下的圖片。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給button設置不同狀態下的圖片
Observable.just(UIImage(named: "idle_0"))
.subscribe(button.rx.image(for: .normal))
.disposed(by: bag)
Observable.just(UIImage(named: "idle_59"))
.subscribe(button.rx.image(for: .highlighted))
.disposed(by: bag)
}
}
-
rx.backgroundImage(for: ...)
方法
rx.backgroundImage(for: ...)
方法是對傳統方式button.setBackgroundImage(..., for: ...)
方法的封裝,我們可以用它來給button設置不同狀態下的背景圖片。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給button設置不同狀態下的圖片
Observable.just(UIImage(named: "idle_1"))
.subscribe(button.rx.backgroundImage(for: .normal))
.disposed(by: bag)
Observable.just(UIImage(named: "idle_58"))
.subscribe(button.rx.backgroundImage(for: .highlighted))
.disposed(by: bag)
}
}
5、UITextField
UITextField的rx.text
屬性是Subjects,所以它既可以發出事件、也可以監聽Observable。
UITextField的rx.isSecureTextEntry
是Observer,所以它可以監聽Observable。
UITextField的rx.controlEvent(...)
方法的返回值是Observable,所以它可以發出事件。
-
rx.text
屬性
rx.text
屬性充當Observable角色時,一般被用來監聽textField內容的改變(注意:通過這種方式來監聽textField內容的改變時,一打開界面就算textField還沒成爲第一響應者也會觸發一次回調)。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來監聽textField內容的改變
textField.rx.text
.subscribe(onNext: { element in
print("textField的內容改變了:\(element)")
})
.disposed(by: bag)
}
}
rx.text
屬性充當Observer角色時,是對textField.setText(...)
方法的封裝,我們可以用它來給textField設置內容。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來給textField設置內容
Observable.just("Hello RxSwift")
.subscribe(textField.rx.text)
.disposed(by: bag)
}
}
-
rx.isSecureTextEntry
屬性
rx.isSecureTextEntry
是對textField.setSecureTextEntry(...)
方法的封裝,我們可以用它來設置textField是否密文輸入。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來設置textField是否密文輸入
Observable.just(true)
.subscribe(textField.rx.isSecureTextEntry)
.disposed(by: bag)
}
}
-
rx.controlEvent(...)
方法
rx.controlEvent(...)
方法是對傳統方式textField一堆代理方法的封裝,我們可以用它來監聽textField的不同狀態,上面的rx.text
屬性底層就是通過這個方法實現的,只不過鎖死了editingChanged狀態。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 用RxSwift來監聽textField的不同狀態
textField.rx.controlEvent(.editingDidBegin)
.subscribe { _ in
// textField開始編輯了、光標開始閃動(textField成爲第一響應者)
print("textFieldDidBeginEditing")
}
.disposed(by: bag)
// 一打開界面不會觸發,只有真正改變了內容纔會觸發
textField.rx.controlEvent(.editingChanged)
.subscribe { [weak self] _ in
// textField的內容改變了
print("textField的內容改變了:\(self.textField.text)")
}
.disposed(by: bag)
textField.rx.controlEvent(.editingDidEnd)
.subscribe { _ in
// textField結束編輯了、光標停止閃動
print("textFieldDidEndEditing")
}
.disposed(by: bag)
textField.rx.controlEvent(.editingDidEndOnExit)
.subscribe { _ in
// 點擊了鍵盤上的return鍵結束編輯,緊接着會觸發【textField結束編輯了、光標停止閃動】
print("textFieldShouldReturn")
}
.disposed(by: bag)
}
}
6、UIScrollView
UIScrollView的rx.contentOffset
屬性是Subjects,所以它既可以發出事件、也可以監聽Observable。
UIScrollView的rx.willBeginDragging
、rx.didScroll
、rx.didEndDragging
、rx.didEndDecelerating
屬性都是Observable,所以它們可以發出事件。
-
rx.contentOffset
屬性
rx.contentOffset
屬性充當Observable角色時,一般被用來監聽scrollView的滾動(注意:通過這種方式來監聽scrollView的滾動時,一打開界面就算不滾動scrollView也會觸發一次回調)。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSize(width: 0, height: 1000)
// 用RxSwift來監聽scrollView的滾動
scrollView.rx.contentOffset
.subscribe(onNext: { contentOffset in
print("scrollView滾動中:\(contentOffset)")
})
.disposed(by: bag)
}
}
rx.contentOffset
屬性充當Observer角色時,是對scrollView.setContentOffset(...)
方法的封裝,我們可以用它來設置內容scrollView的偏移量。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSize(width: 0, height: 1000)
// 用RxSwift來設置內容scrollView的偏移量
Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.map({ element in
let point = CGPoint(x: 0, y: 10 * element) // 映射出點
return point
})
.bind(to: scrollView.rx.contentOffset)
.disposed(by: bag)
}
}
-
rx.willBeginDragging
、rx.didScroll
、rx.didEndDragging
、rx.didEndDecelerating
屬性
這一堆屬性是對傳統方式scrollView一堆代理方法的封裝,我們可以用它們來監聽scrollView的滾動。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSize(width: 0, height: 1000)
// 用RxSwift來監聽scrollView的滾動
scrollView.rx.willBeginDragging
.subscribe { _ in
print("即將開始拖拽scrollView")
}
.disposed(by: bag)
// 一打開界面不會觸發,只有真正滾動了scrollView纔會觸發
scrollView.rx.didScroll
.subscribe { _ in
print("scrollView滾動中:\(self.scrollView.contentOffset)")
}
.disposed(by: bag)
scrollView.rx.didEndDragging
.subscribe { decelerate in
if decelerate.element == false {
print("scrollView停止滾動");
} else {
print("已經停止拖拽scrollView,但是scrollView由於慣性還在減速滾動");
}
}
.disposed(by: bag)
// 但是光靠這個方法來判定scrollView停止滾動是有一個bug的,那就是當我們的手指停止拖拽scrollView時、按住屏幕不放手、導致scrollView不滾動,是不會觸發這個方法的,而是會觸發scrollViewDidEndDragging:willDecelerate:方法,所以嚴謹來判斷應該靠它倆聯合
scrollView.rx.didEndDecelerating
.subscribe { _ in
print("scrollView停止滾動")
}
.disposed(by: bag)
}
}
7、UITableView
UITableView這塊兒,我們就不像上面那樣一個一個屬性或方法詳細說了,直接演示下怎麼用,因爲這塊兒的內容實在太多了,可以自己點進去UITableView+Rx.swift
文件去看去分析。
UITableView這塊兒,Observable通常有兩種,一是tableView要顯示的數據——也就是說我們得把tableView要顯示的數據給手動搞成一個Observable,然後讓tableView的一堆東西——即Observer來監聽這個Observable就可以了,這樣數據發生變化時,tableView的顯示就會跟着自動發生變化,非常符合數據驅動UI的理念;二是用戶對tableView做的操作——如點擊cell、插入cell、刪除cell、移動cell等;其它的都是Observer。
- 實現單個分區的UITableView
1️⃣我們不需要像傳統方式那樣實現numberOfSectionsInTableView:
代理方法告訴tableView有一個分區
2️⃣我們也不需要像傳統方式那樣調用numberOfRowsInSection:
告訴tableView分區裏有多少個cell
3️⃣我們只需要實現cellForRowAtIndexPath:
這一個代理方法(是一個Observer),讓它來監聽數據(是一個Observable)就可以了,RxSwift會自動給我們搞好有一個分區、分區裏有多少個cell這些事
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
let bag = DisposeBag()
// tableView
lazy var tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.backgroundColor = UIColor.clear
return tableView
}()
// tableView要顯示的數據
//
// 不要定義成普通數組,要定義成Observable,讓它發出的事件裏掛數組,這樣才能讓tableView監聽,達到數據驅動UI的效果
var dataArray = Observable.just([
"張一",
"張二",
"張三",
])
override func viewDidLoad() {
super.viewDidLoad()
_addViews()
_layoutViews()
_setupRxSwift()
}
}
// MARK: - setupRxSwift
extension ViewController {
private func _setupRxSwift() {
// 讓tableView.rx.items屬性監聽數據就可以了,就這麼簡單,搞定
//
// dataArray是個Observable
// tableView.rx.items是個Observer(本質就是對cellForRowAtIndexPath:代理方法的封裝)
dataArray
.bind(to: tableView.rx.items) {
tableView, indexPathRow, data in
var cell = tableView.dequeueReusableCell(withIdentifier: "reuseId")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "reuseId")
}
cell?.textLabel?.text = data
return cell!
}
.disposed(by: bag)
}
}
// MARK: - addViews, layoutViews
extension ViewController {
private func _addViews() {
view.addSubview(tableView)
}
private func _layoutViews() {
tableView.frame = view.bounds
}
}
4️⃣那怎麼監聽cell的點擊呢?上面我們說過“用戶對tableView做的操作都是Observable”,所以很簡單,拿個閉包監聽這個Observable就行了
// MARK: - setupRxSwift
extension ViewController {
private func _setupRxSwift() {
// 監聽cell的點擊,獲取那一行對應的row
tableView.rx.itemSelected
.subscribe(onNext: { indexPath in
print("點擊了cell,對應的row爲:\(indexPath.row)")
})
.disposed(by: bag)
// 監聽cell的點擊,獲取那一行對應的data
tableView.rx.modelSelected(String.self)
.subscribe(onNext: { data in
print("點擊了cell,對應的data爲:\(data)")
})
.disposed(by: bag)
}
}
5️⃣要想設置cell的高度、section header和section header的高度、section footer和section footer的高度,得通過Rx提供的方法設置代理並實現相關的代理方法
// MARK: - setupRxSwift
extension ViewController {
private func _setupRxSwift() {
tableView.rx.setDelegate(self).disposed(by: bag)
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "我是區頭"
return label
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "我是區尾"
return label
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 44
}
}
6️⃣至於用戶能對tableView做的其它編輯操作——如插入cell、刪除cell、移動cell等,你只要知道它們肯定都是Observable就行了,找相應的API實現即可。
- 實現多個分區的UITableView
要想實現多個分區的UITableView,必須得安裝RxDataSources
這個框架。
pod 'RxSwift', '~> 5.0'
pod 'RxCocoa', '~> 5.0'
pod 'RxDataSources', '~> 5.0'
然後在想使用的地方導入這個框架。
// 這個框架的本質就是使用RxSwift對UITableView和UICollectionView的數據源做了一層包裝,使用它可以大大減少我們的工作量
import RxDataSources
1️⃣同樣地,我們只需要實現cellForRowAtIndexPath:
這一個代理方法(是一個Observer),讓它來監聽數據(是一個Observable)就可以了,RxSwift會自動給我們搞好有多少個分區、每個分區裏有多少個cell這些事,不過需要注意的是:RxDataSources是專門用來做多分區的,所以在給它傳數據時,dataArray裏就不能是普通的數據,而必須得是SectionModel或其子類的數據,這也很好理解,一個section對應一個sectionModel嘛,跟單分區的UITableView就這個地方有區別:構建數據 + 數據綁定到tableView。
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class ViewController: UIViewController {
let bag = DisposeBag()
// tableView
lazy var tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.backgroundColor = UIColor.clear
return tableView
}()
// tableView要顯示的數據
//
// 不要定義成普通數組,要定義成Observable,讓它發出的事件裏掛數組,這樣才能讓tableView監聽,達到數據驅動UI的效果
var dataArray = Observable.just([
// 數據這兒有幾個CustomSectionModel,tableView就會有幾個分區
CustomSectionModel(identityText: "", items: [
// 每個分區裏的數據
"張一",
"張二",
"張三",
]),
CustomSectionModel(identityText: "", items: [
"李一",
"李二",
"李三",
"李四",
]),
CustomSectionModel(identityText: "", items: [
"王一",
"王二",
"王三",
"王四",
"王五",
]),
])
override func viewDidLoad() {
super.viewDidLoad()
_addViews()
_layoutViews()
_setupRxSwift()
}
}
// MARK: - setupRxSwift
extension ViewController {
private func _setupRxSwift() {
// 讓tableView.rx.items(...)方法的返回值監聽數據就可以了,就這麼簡單,搞定
//
// dataArray是個Observable
// tableView.rx.items(...)方法的返回值是個Observer
dataArray
.bind(to: tableView.rx.items(dataSource: RxTableViewSectionedReloadDataSource<CustomSectionModel>( configureCell: {
(dataSource, tableView, indexPath, data) -> UITableViewCell in
var cell = tableView.dequeueReusableCell(withIdentifier: "reuseId")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "reuseId")
}
cell?.textLabel?.text = data
return cell!
})))
.disposed(by: bag)
tableView.rx.setDelegate(self).disposed(by: bag)
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "我是區頭"
return label
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
}
// MARK: - addViews, layoutViews
extension ViewController {
private func _addViews() {
view.addSubview(tableView)
}
private func _layoutViews() {
tableView.frame = view.bounds
}
}
// MARK: - 自定義SectionModel
struct CustomSectionModel {
// 該分區的唯一標識,可以定義成任意類型
// 這裏我們定義成String類型了,沒什麼特殊需求的話傳個空字符串""進來就行了
var identityText: String
// 該分區裏的數據
// 這裏我們分區裏的數據都是String類型
var items: [String]
}
extension CustomSectionModel: AnimatableSectionModelType {
var identity: String {
return identityText
}
init(original: CustomSectionModel, items: [String]) {
self = original
self.items = items
}
}
2️⃣其它的實現跟單分區的UITableView一樣。
8、UICollectionView
UICollectionView和UITableView差不多。
二、然後我們寫個MVVM的小案例
需求很簡單:
- 用一個tableView顯示用戶的姓名、性別、年齡
- textField輸入“張”時就請求張姓的用戶顯示,textField輸入“李”時就請求李姓的用戶顯示
1、不使用RxSwift時的MVVM
- Model層:
PersonModel.swift
/*
Model層的數據搞成最原始的數據即可
*/
import UIKit
class PersonModel: NSObject {
/// 姓名
var name: String?
/// 性別
///
/// 0-未知,1-男,2-女
var sex: Int?
/// 年齡
var age: Int?
init(dict: [String : Any]) {
name = dict["name"] as? String
sex = (dict["sex"] as? NSNumber)?.intValue
age = (dict["age"] as? NSNumber)?.intValue
}
}
- View層:
TableViewCell.swift
/*
View層持有vm,直接拿着數據展示即可
*/
import UIKit
class TableViewCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var sexLabel: UILabel!
@IBOutlet weak var ageLabel: UILabel!
var personVM: PersonViewModel? {
didSet {
guard let personVM = personVM else { return }
nameLabel.text = personVM.name
sexLabel.text = personVM.sex
ageLabel.text = "\(personVM.age)"
}
}
}
- ViewModel層:
PersonViewModel.swift
/*
1、ViewModel層:負責請求數據
2、ViewModel層:負責處理數據
3、ViewModel層:負責存儲數據
*/
import UIKit
class PersonViewModel {
// 持有一個_personModel,以便處理數據:VM一對一Model地添加屬性並處理,搞成計算屬性即可
private var _personModel: PersonModel?
init(personModel: PersonModel? = nil) {
_personModel = personModel
}
/// vm數組
///
/// 真正暴露給外面使用的是vm數組,裏面的數據已經處理好了,直接拿着顯示就行了
lazy var personVMArray = [PersonViewModel]()
/// 姓名
var name: String {
return _personModel?.name ?? ""
}
/// 性別
///
/// 0-未知,1-男,2-女
var sex: String {
if _personModel?.sex == 1 {
return "男"
} else if _personModel?.sex == 2 {
return "女"
} else {
return "未知"
}
}
/// 年齡
var age: Int {
return _personModel?.age ?? 0
}
}
// MARK: - 請求數據
extension PersonViewModel {
/// 請求數據
func loadData(params: String, completionHandler: @escaping (_ isSuccess: Bool) -> Void) {
guard let path = Bundle.main.path(forResource: params, ofType: ".plist") else {
completionHandler(false)
return
}
guard let array = NSArray(contentsOfFile: path) as? [[String : Any]] else {
completionHandler(false)
return
}
for dict in array {
let personModel = PersonModel(dict: dict)
let personVM = PersonViewModel(personModel: personModel)
personVMArray.append(personVM)
completionHandler(true)
}
}
}
- Controller層:
ViewController.swift
/*
1、Controller層:持有view,創建並把view添加到界面上
2、Controller層:持有vm,調用vm的方法請求數據
3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過block的方式告訴Controller的,Controller可以調用一下view的reloadData方法把vm裏最新存儲的數據賦值給view顯示
4、view --> vm:view產生的變化是通過代理告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
*/
import UIKit
class ViewController: UIViewController {
private lazy var _textField: UITextField = {
let textField = UITextField()
textField.backgroundColor = UIColor.red
textField.returnKeyType = .done
textField.delegate = self
return textField
}()
private lazy var _tableView: UITableView = {
let tableView = UITableView()
tableView.backgroundColor = UIColor.green
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
private lazy var _personVM = PersonViewModel()
private var _insertText = "張";
override func viewDidLoad() {
super.viewDidLoad()
_addViews()
_layoutViews()
_loadData(params: _insertText)
}
}
// MARK: - UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
_insertText = textField.text ?? ""
_personVM.loadData(params: _insertText) { isSuccess in
if isSuccess {
self._tableView.reloadData()
} else {
print("請求數據出錯")
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
view.endEditing(true)
return true
}
}
// MARK: - UITableViewDataSource, UITableViewDelegate
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return _personVM.personVMArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.personVM = _personVM.personVMArray[indexPath.row];
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
// MARK: - 請求數據
extension ViewController {
private func _loadData(params: String) {
_personVM.loadData(params: params) { isSuccess in
if isSuccess {
self._tableView.reloadData()
} else {
print("請求數據出錯")
}
}
}
}
// MARK: - setupUI
extension ViewController {
private func _addViews() {
view.addSubview(_textField)
view.addSubview(_tableView)
}
private func _layoutViews() {
_textField.frame = CGRect(x: 0, y: 20, width: view.bounds.size.width, height: 44)
_tableView.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
}
}
2、使用RxSwift時的MVVM
Model層和View層都不需要改動
ViewModel層:
PersonViewModel.swift
/*
1、ViewModel層:負責請求數據
2、ViewModel層:負責處理數據
3、ViewModel層:負責存儲數據
*/
import UIKit
import RxSwift
class PersonViewModel {
// 持有一個_personModel,以便處理數據:VM一對一Model地添加屬性並處理,搞成計算屬性即可
private var _personModel: PersonModel?
init(personModel: PersonModel? = nil) {
_personModel = personModel
}
//-----------變化1-----------//
/// 同時新增一個跟原來同名的vm數組,使用RxSwift:
/// 1、因爲外面tableView要顯示的數據就是這個數組裏的數據,換句話說tableView要監聽這個數組,所以這個數組就不能再定義成普通的數據了,而應該定義成一個Observable,裏面的事件掛的數據是數組
/// 2、又因爲這個數組裏的數據會隨着textField輸入文本的變化而變化,換句話說這個數組應該監聽textField的文本變化,所以這個數組應該定義成一個Observer
/// 3、所以最終我們得把這個數組定義成一個Subjects
var personVMArray = PublishSubject<[PersonViewModel]>()
/// 我們把原來的personVMArray直接降級成一個私有屬性,繼續搞它原來負責的事情
private lazy var _personVMArray = [PersonViewModel]()
//-----------變化1-----------//
/// 姓名
var name: String {
return _personModel?.name ?? ""
}
/// 性別
///
/// 0-未知,1-男,2-女
var sex: String {
if _personModel?.sex == 1 {
return "男"
} else if _personModel?.sex == 2 {
return "女"
} else {
return "未知"
}
}
/// 年齡
var age: Int {
return _personModel?.age ?? 0
}
}
// MARK: - 請求數據
extension PersonViewModel {
/// 請求數據
func loadData(params: String, completionHandler: @escaping (_ isSuccess: Bool) -> Void) {
guard let path = Bundle.main.path(forResource: params, ofType: ".plist") else {
completionHandler(false)
return
}
guard let array = NSArray(contentsOfFile: path) as? [[String : Any]] else {
completionHandler(false)
return
}
_personVMArray.removeAll()
for dict in array {
let personModel = PersonModel(dict: dict)
let personVM = PersonViewModel(personModel: personModel)
_personVMArray.append(personVM)
}
completionHandler(true)
//-----------變化2-----------//
// 請求成功後,Observable發出一個next事件把數據發出去
personVMArray.onNext(_personVMArray)
//-----------變化2-----------//
}
}
- Controller層:
ViewController.swift
/*
1、Controller層:持有view,創建並把view添加到界面上
2、Controller層:持有vm,調用vm的方法請求數據
3、vm --> view:Controller調用vm的方法請求數據,請求完成後vm是通過block的方式告訴Controller的,但是Controller只需要處理出錯的情況,成功的情況什麼都不需要做,因爲數據已經自動驅動UI了——數據已經綁定到tableView上了
4、view --> vm:view產生的變化不再是通過代理而是通過Rx的鏈式調用告訴Controller的,Controller可以調用vm的方法把view發生的變化告訴它
*/
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
let bag = DisposeBag()
private lazy var _textField: UITextField = {
let textField = UITextField()
textField.backgroundColor = UIColor.red
textField.returnKeyType = .done
return textField
}()
private lazy var _tableView: UITableView = {
let tableView = UITableView()
tableView.backgroundColor = UIColor.green
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
return tableView
}()
private lazy var _personVM = PersonViewModel()
private var _insertText = "張";
override func viewDidLoad() {
super.viewDidLoad()
// 數據已經在VM裏搞定了,所以這裏先搞定UI
_addViews()
_layoutViews()
// 再把數據和UI進行雙向綁定
_setupRxSwift()
// 初始請求數據
_loadData(params: _insertText)
}
}
// MARK: - setupRxSwift
extension ViewController {
private func _setupRxSwift() {
// 數據綁定到tableView上:數據驅動UI
_personVM.personVMArray
.bind(to: _tableView.rx.items) {
tableView, indexPathRow, data in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: IndexPath(row: indexPathRow, section: 0)) as! TableViewCell
cell.personVM = data
return cell
}
.disposed(by: bag)
_tableView.rx.setDelegate(self).disposed(by: bag)
// textField的內容發生變化後,重新請求數據:UI驅動數據
_textField.rx.controlEvent(.editingDidEnd)
.subscribe { _ in
// textField結束編輯了、光標停止閃動
self._insertText = self._textField.text ?? ""
self._personVM.loadData(params: self._insertText) { isSuccess in
if !isSuccess {
print("請求數據出錯")
}
}
}
.disposed(by: bag)
_textField.rx.controlEvent(.editingDidEndOnExit)
.subscribe { _ in
// 點擊了鍵盤上的return鍵結束編輯,緊接着會觸發【textField結束編輯了、光標停止閃動】
print("textFieldShouldReturn")
}
.disposed(by: bag)
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
// MARK: - 請求數據
extension ViewController {
private func _loadData(params: String) {
_personVM.loadData(params: params) { isSuccess in
if !isSuccess {
print("請求數據出錯")
}
}
}
}
// MARK: - setupUI
extension ViewController {
private func _addViews() {
view.addSubview(_textField)
view.addSubview(_tableView)
}
private func _layoutViews() {
_textField.frame = CGRect(x: 0, y: 20, width: view.bounds.size.width, height: 44)
_tableView.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
}
}
參考
1、RxSwift中文文檔
2、RxSwift大全