目录
一、首先我们看看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.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.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.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.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( 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大全