一、Swift中的协议
协议的介绍
协议的定义方式
protocol CustomProtocol {
// 这里是协议的定义部分
}
协议的属性
协议可以要求遵循协议的类型提供特定名称和类型的实例属性
或类型属性
。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型
。
1.协议要求一个属性必须明确
是可读的/可读可写
的,类型声明后加上 { set get }
来表示属性是可读可写的;
2.属性要求定义为变量类型
,即使用var
而不是let。
protocol CustomProtocol {
var enableReadOrWritetable: Int { get set } //可读可写
var enbaleOnlyReadtable: Int { get } //可读
}
协议的方法
1.自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:
)分隔。遵循多个协议
时,各协议之间用逗号(,
)分隔。
2.若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔。
3.在协议中定义方法
,只需要定义当前方法的名称、参数列表和返回值
。类遵循了协议,必须实现协议中的方法。
class Person: CustomProtocol{
func eating() {
print("Person eating")
}
static func doSomething() {
print("Person teach")
}
}
var t = Person()
p.eating()
Person.doSomething()
4.协议中也可以定义初始化方法
,当实现初始化器时,必须使用required
关键字。
protocol CustomProtocol {
init(age: Int)
}
class Person: CustomProtocol {
var age: Int
required init(age: Int) {
self.age = age
}
}
5.如果一个协议只能被类实现
,需要协议继承自AnyObject
。
6.如果一个协议协议继承自AnyObject,
结构体遵守该协议时,会报错。
注意:实现协议中的 mutating
方法时,若是类类型
,则不用写mutating
关键字。而对于结构体和枚举
,则必须写mutating
关键字。
swift 的结构体和枚举中,mutating 在方法中改变方法所属的实例。
protocol Togglable {
mutating func toggle()
}
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
协议作为类型
协议可以被当做一个功能完备的类型来使用,
将协议作为类型
,主要有以下3种情况👇
- 作为
函数、方法或者初始化
程序中的参数类型
或者返回值
- 作为
常量、变量或属性
的类型
- 作为
数组、字典或者其他容器
中元素Item的类型
事例:用集成实现圆和长方体的面积
一、用继承的方式
class Shape{
var area: Double{
get{
return 0
}
}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
override var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
override var area: Double{
get{
return width * height
}
}
}
var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
print(shape.area)
}
二、用协议的方法
protocol Shape {
var area: Double {get}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Int
init(_ width: Int, _ height: Int) {
self.width = width
self.height = height
}
var area: Double{
get{
return Double(width * height)
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10, 20)
//所谓的多态:根据具体的类来决定调度的方法
var shapes: [Shape] = [circle, rectangle]
//这里能区分不同area的原因是因为 在protocol中存放了pwt(协议目录表),可以根据这个表来正确调用对应的实现方法(pwt中也是通过class_method查找,
//同时在运行过程中也记录了metadata,在pwt中通过metadata查找V-Table,从而完成当前方法的调用)
for shape in shapes{
print(shape.area)
}
//打印结果:314.0 200.0
协议
在底层的存储结构
1.前24个
字节,主要用于存储遵循了协议
的class/struct
的属性值
,如果24字节不够存储
,会在堆区
开辟一个内存空间,然后在24字节中的前8个字节
存储该堆区地址
(超出24字节是直接分配堆区空间
,然后存储值,并不是先存储值
,然后发现不够再分配堆区空间)
2.后16个字节
分别用于存储vwt(值目录表)、pwt(协议目录表)
二、看RxCocoa做了什么
为了帮助我们更加简单优雅地实现ViewModel和View的双向绑定,RxCocoa已经帮我们把UIKit框架里常用控件的常用属性都搞成了Observable或Binder、有的属性甚至是Subjects。
//
// TestMVVMViewController.swift
// SwiftStudyDemo
//
// Created by denggaoqiang on 2022/3/13.
//
import UIKit
import RxSwift
import RxCocoa
class TestMVVMViewController: UIViewController {
private lazy var textViewModel: TestMVVMViewModel = {
let model = TestMVVMViewModel()
return model
}()
private lazy var textView: TestMMVVMTextView = {
let view = TestMMVVMTextView(viewModel: self.textViewModel)
view.frame = UIScreen.main.bounds
return view
}()
private lazy var customView: UIView = {
let view = UIView.init(frame: CGRectMake(0, 100, 200, 30))
view.backgroundColor = UIColor.white
return view
}()
private lazy var customBtn: UIButton = {
let view = UIButton.init(type: .custom)
view.backgroundColor = UIColor.blue
view.frame = CGRect(x: 100, y: 170, width: UIScreen.main.bounds.width-200, height: 40)
view.addTarget(self, action:#selector(clickTheButton), for: .touchUpInside)
return view
}()
private lazy var customLabel: UILabel = {
let view = UILabel.init(frame: CGRectMake(0, 250, 200, 30))
view.backgroundColor = UIColor.white
return view
}()
private lazy var customImgView: UIImageView = {
let view = UIImageView.init(frame: CGRectMake(0, 300, 100, 30))
view.backgroundColor = UIColor.black
return view
}()
private lazy var customRxBtn: UIButton = {
let view = UIButton.init(type: .custom)
view.backgroundColor = UIColor.blue
view.frame = CGRect(x: 100, y: 350, width: UIScreen.main.bounds.width-200, height: 40)
return view
}()
private lazy var customRxTextField: UITextField = {
let view = UITextField.init(frame: CGRectMake(0, 400, 300, 30))
view.backgroundColor = UIColor.white
return view
}()
private lazy var customRxScrollView: UIScrollView = {
let view = UIScrollView.init(frame: CGRectMake(0, 400, 300, 30))
view.backgroundColor = UIColor.white
view.contentSize = CGSizeMake(100, 1000)
return view
}()
let bag = DisposeBag()
var customColor:UIColor = UIColor.blue
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
// view.addSubview(customView)
// view.addSubview(customBtn)
// view.addSubview(customLabel)
// view.addSubview(customImgView)
// view.addSubview(customRxBtn)
// view.addSubview(customRxTextField)
view.addSubview(customRxScrollView)
// rxSwift_UIView()
// rxSwift_UILabel()
// rxSwift_UIImageView()
// rxSwift_UIButton()
// rxSwift_UITextField()
rxSwift_UIScrollView()
}
@objc func clickTheButton(sender:UIButton){
print("传统意义上的按钮点击")
sender.backgroundColor = UIColor.green
// 用RxSwift来给textField设置内容
Observable.just("Hello RxSwift")
.subscribe(customRxTextField.rx.text)
.disposed(by: bag)
}
//RXSwift -- UIView
func rxSwift_UIView() {
// 用RxSwift来设置customView的背景颜色
//1、observable负责发出事件,事件上挂的数据就是一个颜色
//2、customView的rx.backgroundColor属性就是一个binder,所以它可以监听observable,当它收到observable发出的事件时,就会把事件上挂的颜色拿下来真正赋值给customView的backgroundColor属性。还记得我们自己是怎么创建Binder的吧,可以翻回去看一下,RxCocoa底层就是那么实现的
let observable = Observable.just(UIColor.black)
let binder = customView.rx.backgroundColor
observable.bind(to: binder).disposed(by: bag)
// 用RxSwift来设置customView的透明度
//rx.alpha属性是对传统方式view.setAlpha(...)方法的封装,我们可以用它来设置view的透明度。
Observable.just(0.618).bind(to: customView.rx.alpha).disposed(by: bag)
// 用RxSwift来设置customView是否隐藏
//rx.isHidden属性是对传统方式view.setHidden(...)方法的封装,我们可以用它来设置view是否隐藏。
Observable.just(false).bind(to: customView.rx.isHidden).disposed(by: bag)
// 用RxSwift来设置customView是否能够处理用户交互
//rx.isUserInteractionEnabled属性是对传统方式view.setUserInteractionEnabled(...)方法的封装,我们可以用它来设置view是否能够处理用户交互。
Observable.just(true).bind(to: customView.rx.isUserInteractionEnabled).disposed(by: bag)
}
//RXSwift -- UILabel
func rxSwift_UILabel() {
// 用RxSwift来设置label的文本
//rx.text属性是对传统方式label.setText(...)方法的封装,我们可以用它来设置label的文本。
Observable.just("Hello RxSwift").bind(to: customLabel.rx.text).disposed(by: bag)
// 用RxSwift来设置label的属性文本
//rx.attributedText属性是对传统方式label.setAttributedText(...)方法的封装,我们可以用它来设置label的属性文本。
Observable.just("Hello RxSwift").map({ element in
print(element)
let attributedString = NSAttributedString(string: element, attributes: [
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
NSAttributedString.Key.underlineColor: UIColor.red,
])
return attributedString
}).bind(to: customLabel.rx.attributedText).disposed(by: bag)
}
//RXSwift -- UIImageView
func rxSwift_UIImageView() {
// 用RxSwift来设置imageView的图片
//rx.image属性是对传统方式imageView.setImage(...)方法的封装,我们可以用它来设置imageView的图片。
Observable.just(UIImage.init(named: "idle_0")).bind(to: customImgView.rx.image).disposed(by: bag)
}
func rxTimerTest() {
//诸如NSTimer(Timer)、CADisplayLink都是需要加入到RunLoop中的,这样的定时器都会收到RunLoop的影响;而GCD的定时器DispatchSource则和RunLoop无关,不会受到滑动事件的影响。那我们就可以大胆的猜测,RxSwift封装的定时器也是对DispatchSource的封装。
//dueTime:从现在到初始化第一次的触发定时器的时间
//period:初始化之后每次触发的时间间隔
//scheduler:调度类型
let rxTimer = Observable<Int>.timer(.seconds(5), period: .milliseconds(2), scheduler: MainScheduler.instance)
rxTimer.subscribe(onNext: { (num) in
print("==\(num)==")
}).disposed(by: bag)
}
//RXSwift -- UIButton
func rxSwift_UIButton() {
//rx.tap属性是对传统方式button.addTarget(..., action: #selector(...), for: .touchUpInside)方法的封装,我们可以用它来给button添加touchUpInside状态下的点击事件。
// 用RxSwift来给button添加touchUpInside状态下的点击事件
customRxBtn.rx.tap.subscribe { _ in
print("RX按钮被点击了")
}.disposed(by: bag)
//rx.isEnabled属性是对传统方式button.setEnabled(...)方法的封装,我们可以用它来设置button是否可以点击。
// 用RxSwift来设置button是否可以点击
Observable.just(true).bind(to: customRxBtn.rx.isEnabled).disposed(by: bag)
//rx.controlEvent(...)方法是对传统方式button.addTarget(..., action: ..., for: ...)方法的封装,我们可以用它来给button添加任意状态下的点击事件,上面的rx.tap属性底层就是通过这个方法实现的,只不过锁死了touchUpInside状态。
// 用RxSwift来给button添加任意状态下的点击事件
customRxBtn.rx.controlEvent(.touchDown).subscribe { _ in
print("touchDown")
}.disposed(by: bag)
// 用RxSwift来给button设置不同状态下的文本
Observable.just("正常状态下的文本").subscribe(customRxBtn.rx.title(for: .normal))
.disposed(by: bag)
Observable.just("高亮状态下的文本").subscribe(customRxBtn.rx.title(for: .highlighted))
.disposed(by: bag)
}
//RXSwift -- UITextField
func rxSwift_UITextField() {
//rx.text属性充当Observable角色时,一般被用来监听textField内容的改变(注意:通过这种方式来监听textField内容的改变时,一打开界面就算textField还没成为第一响应者也会触发一次回调)。
// 用RxSwift来监听textField内容的改变
customRxTextField.rx.text.subscribe(onNext: { element in
print("textField的内容改变了:\(String(describing: element))")
}).disposed(by: bag)
// 用RxSwift来设置textField是否密文输入
Observable.just(true).subscribe(customRxTextField.rx.isSecureTextEntry).disposed(by: bag)
// 用RxSwift来监听textField的不同状态
customRxTextField.rx.controlEvent(.editingDidBegin).subscribe { _ in
// textField开始编辑了、光标开始闪动(textField成为第一响应者)
print("textFieldDidBeginEditing")
}.disposed(by: bag)
// 一打开界面不会触发,只有真正改变了内容才会触发
customRxTextField.rx.controlEvent(.editingChanged).subscribe { [weak self] _ in
// textField的内容改变了
print("textField的内容改变了:\(String(describing: self?.customRxTextField.text))")
}.disposed(by: bag)
customRxTextField.rx.controlEvent(.editingDidEnd).subscribe { _ in
// textField结束编辑了、光标停止闪动
print("textFieldDidEndEditing")
}.disposed(by: bag)
customRxTextField.rx.controlEvent(.editingDidEndOnExit).subscribe { _ in
// 点击了键盘上的return键结束编辑,紧接着会触发【textField结束编辑了、光标停止闪动】
print("textFieldShouldReturn")
}.disposed(by: bag)
}
//RXSwift -- UIScrollView
func rxSwift_UIScrollView() {
//rx.contentOffset属性充当Observable角色时,一般被用来监听scrollView的滚动(注意:通过这种方式来监听scrollView的滚动时,一打开界面就算不滚动scrollView也会触发一次回调)。
// 用RxSwift来监听scrollView的滚动
customRxScrollView.rx.contentOffset
.subscribe(onNext: { contentOffset in
print("scrollView滚动中:\(contentOffset)")
}).disposed(by: bag)
//rx.contentOffset属性充当Observer角色时,是对scrollView.setContentOffset(...)方法的封装,我们可以用它来设置内容scrollView的偏移量。
Observable.just(CGPointMake(0, 400)).subscribe(customRxScrollView.rx.contentOffset).disposed(by: bag)
// 用RxSwift来监听scrollView的滚动
customRxScrollView.rx.willBeginDragging.subscribe { _ in
print("即将开始拖拽scrollView")
}.disposed(by: bag)
// 一打开界面不会触发,只有真正滚动了scrollView才会触发
customRxScrollView.rx.didScroll
.subscribe { _ in
print("scrollView滚动中:\(self.customRxScrollView.contentOffset)")
}.disposed(by: bag)
customRxScrollView.rx.didEndDragging
.subscribe { decelerate in
if decelerate.element == false {
print("scrollView停止滚动");
} else {
print("已经停止拖拽scrollView,但是scrollView由于惯性还在减速滚动");
}
}.disposed(by: bag)
// 但是光靠这个方法来判定scrollView停止滚动是有一个bug的,那就是当我们的手指停止拖拽scrollView时、按住屏幕不放手、导致scrollView不滚动,是不会触发这个方法的,而是会触发scrollViewDidEndDragging:willDecelerate:方法,所以严谨来判断应该靠它俩联合
customRxScrollView.rx.didEndDecelerating.subscribe { _ in
print("scrollView停止滚动")
}.disposed(by: bag)
}
}
三、
RXSwift 的subject
在使用RXSwift进行响应式编程时,我们经常会遇到需要在不同的线程中发送和订阅事件的场景。RXSwift提供了一种称为Subject的特殊类型,Subject是RXSwift中一种可以同时充当Observable和Observer的类型。它即可以接受事件,也可以发送事件。
Subject类型
四种不同类型的Subject
- PublishSubject
- BehaviorSubject
- ReplaySubject
- Variable(已在RXSwift4.0中弃用)
PublishSubject
PublishSubject它只会向订阅者发送在订阅之后的事件,如果在订阅之前已经发送了事件,那么订阅者将无法接受到这些事件。
let subject = PublishSubject<String>()
subject.onNext("Event1")
let subscription1 = subject.subscribe { item in
print("subscription1 :\(item)")
}
subject.onNext("Event2")
let subscription2 = subject.subscribe { item in
print("subscription2 :\(item)")
}
subject.onNext("Event3")
打印结果:
subscription1 :next(Event2)
subscription1 :next(Event3)
subscription2 :next(Event3)
subscription1在订阅之后才收到了"Event2"和"Event3"两个事件,而subscription2在订阅后才收到"Event3"这个事件
BehaviorSubject
BehaviorSubject是另一种常用的Subject类型。它会向订阅者发送最新的事件以及之后的事件。当有新的订阅者订阅BehaviorSubject时,它会立即接收到最新的事件。
let subject = BehaviorSubject<String>(value: "InitialEvent")
subject.onNext("Event3")
let subscription1 = subject.subscribe { item in
print("subscription1 :\(item)")
}
subject.onNext("Event1")
let subscription2 = subject.subscribe { item in
print("subscription2 :\(item)")
}
subject.onNext("Event2")
打印结果:
subscription1 :next(Event3)
subscription1 :next(Event1)
subscription2 :next(Event1)
subscription1 :next(Event2)
subscription2 :next(Event2)
subscription1在订阅之后先收到上次最新的"Event3"(假如没有subject.onNext("Event3"),会先触发初始化的"InitialEvent"),当subject发送"Event1"事件后, subscription1收到"Event1",subscription2在订阅后先收到上次最新的"Event1",当subject发送"Event2"事件后, subscription1收到"Event2", subscription2也收到"Event2"。
ReplaySubject
ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。
let disposeBag = DisposeBag()
let subject = ReplaySubject<String>.create(bufferSize: 1)
subject.subscribe { item in
print("Subscription: 1 Event:\(item)")
}.disposed(by: disposeBag)
subject.onNext("Event1")
subject.onNext("Event2")
subject.subscribe { item in
print("Subscription: 2 Event:\(item)")
}.disposed(by: disposeBag)
subject.onNext("Event3")
subject.onNext("Event4")
打印结果:
Subscription: 1 Event:next(Event1)
Subscription: 1 Event:next(Event2)
Subscription: 2 Event:next(Event2)
Subscription: 1 Event:next(Event3)
Subscription: 2 Event:next(Event3)
Subscription: 1 Event:next(Event4)
Subscription: 2 Event:next(Event4)
bufferSize为1,create函数会创建一个ReplayOne对象,Subscription: 2收到了"Event2"事件,如果bufferSize为2,create函数会创建一个Replaymany对象。
RXSwift线程安全
由于Subject是一种共享数据的类型,当多个线程同时访问一个Subject时,可能会导致线程安全问题,RXSwift提供了一个特殊的操作符observeOn,可以用来指定订阅者在哪个线程上接受事件。
let subject = PublishSubject<String>()
let queue = DispatchQueue(label: "custom_queue")
subject.observeOn(ConcurrentDispatchQueueScheduler(queue: queue)).subscribe { item in
print("Event received on: \(item),\(Thread.current)")
}
subject.onNext("Event1")
我们使用observeOn操作符将订阅者的线程切换到自定义的ConcurrentDispatchQueueScheduler队列上。这样可以确保订阅者在指定的队列上接受事件。
四、使用 RxSwift + MVVM 构建ViewModel
方案一:采用Subjects
定义ViewModelType协议:
protocol ViewModelType {
associatedtype Input
associatedtype Output
var input: Input { get }
var output: Output { get }
}
这样,我们就可以完全自由地选择何时提供输入、何时订阅输出了。Subject
可以同时充当Observer
和Observable
,把命令式的编程变为Rx的函数式编程。
定义采用Subject
的ViewModel:
final class SayHelloViewModel: ViewModelType {
let input: Input
let output: Output
struct Input {
let name: AnyObserver<String>
let validate: AnyObserver<Void>
}
struct Output {
let greeting: Driver<String>
}
private let nameSubject = ReplaySubject<String>.create(bufferSize: 1)
private let validateSubject = PublishSubject<Void>()
init() {
let greeting = validateSubject
.withLatestFrom(nameSubject)
.map { name in
return "Hello \(name)!"
}
.asDriver(onErrorJustReturn: ":-(")
self.output = Output(greeting: greeting)
self.input = Input(name: nameSubject.asObserver(), validate: validateSubject.asObserver())
}
}
这里有几点值得注意的内容:
1.ViewModel的任务还是输入Input产出Output;
2.Subjects
是private
的,所以你只能通过input和output属性与ViewModel交互;
3.兼具可插拔、可测试的特性,并且充分利用了RxSwift的绑定机制;
View部分的实现:
/// Every view interacting with a `SayHelloViewModel` instance can conform to this.
protocol SayHelloViewModelBindable {
var disposeBag: DisposeBag? { get }
func bind(to viewModel: SayHelloViewModel)
}
/// TableViewCells
final class TextFieldCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var nameTextField: UITextField!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
// Clean Rx subscriptions
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
nameTextField.rx
.text
.orEmpty
.bind(to: viewModel.input.name)
.disposed(by: bag)
disposeBag = bag
}
}
final class ButtonCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var validateButton: UIButton!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
validateButton.rx
.tap
.bind(to: viewModel.input.validate)
.disposed(by: bag)
disposeBag = bag
}
}
final class GreetingCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var greetingLabel: UILabel!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
viewModel.output.greeting
.drive(greetingLabel.rx.text)
.disposed(by: bag)
disposeBag = bag
}
}
/// View
class TableViewController: UIViewController, UITableViewDataSource {
static let cellIdentifiers = [
"TextFieldCell",
"ButtonCell",
"GreetingCell"
]
@IBOutlet weak var tableView: UITableView!
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableViewController.cellIdentifiers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewController.cellIdentifiers[indexPath.row])
(cell as? SayHelloViewModelBindable)?.bind(to: viewModel)
return cell!
}
}
方案二:不采用Subjects
定义ViewModelType协议
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
这种方案简单易行,只需要一次性提供Input给ViewModel,然后ViewModel即可给出Output。
创建SayHelloViewModel,它需要知道输入的文本以及按钮点击事件,这就是Input。
然后Output是文本内容。
final class SayHelloViewModel: ViewModelType {
struct Input {
let name: Observable<String>
let validate: Observable<Void>
}
struct Output {
let greeting: Driver<String>
}
func transform(input: Input) -> Output {
let greeting = input.validate
.withLatestFrom(input.name)
.map { name in
return "Hello \(name)!"
}
.startWith("")
.asDriver(onErrorJustReturn: ":-(")
return Output(greeting: greeting)
}
}
创建SayHelloViewController:
final class SayHelloViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var validateButton: UIButton!
@IBOutlet weak var greetingLabel: UILabel!
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
let inputs = SayHelloViewModel.Input(name: nameTextField.rx.text.orEmpty.asObservable(),
validate: validateButton.rx.tap.asObservable())
let outputs = viewModel.transform(input: inputs)
outputs.greeting
.drive(greetingLabel.rx.text)
.disposed(by: bag)
}
}
ViewModel应该是可插拔的,那么我们可以把之前定义的ViewModel用于其他View吗?
现在,如果我们尝试将之前的ViewModel用于带有TableView的View,会发生什么事情?
/// TableViewCells
final class TextFieldCell: UITableViewCell {
@IBOutlet weak var nameTextField: UITextField!
}
final class ButtonCell: UITableViewCell {
@IBOutlet weak var validateButton: UIButton!
}
final class GreetingCell: UITableViewCell {
@IBOutlet weak var greetingLabel: UILabel!
}
/// ViewController
final class SayHelloViewController: UIViewController, UITableViewDataSource {
static let cellIdentifiers = [
"TextFieldCell",
"ButtonCell",
"GreetingCell"
]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableViewController.cellIdentifiers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Classic dequeue work
}
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
// Let's discuss about this
let inputs = SayHelloViewModel.Input(name: 😱😱, validate: 😱😱)
}
}
然而,我们甚至无法为ViewModel提供Input。因为我们不能在创建ViewModel时就获取到UITableView的内容。所以,使用这种方案有一个前提条件:在创建ViewModel的Input时,可以获得全部所需的资源。
这时,你就需要采用第一种方案了!
总结:
需要根据需要来决定采用哪一种方案。
第一种方案兼容性强,但是定义及使用都略显繁琐。
第二种方案简单易行,但是有一定的局限性。
另一种方案
Model层:PersonModel.swift
import UIKit
class PersonModel: NSObject {
/// 姓名
var name: String?
/// 性别
///
/// 0-未知,1-男,2-女
var sex: Int?
/// 年龄
var age: Int?
init(name: String? = nil, sex: Int? = nil, age: Int? = nil) {
self.name = name
self.sex = sex
self.age = age
}
init(dict:[String:Any]) {
name = dict["name"] as? String
sex = (dict["sex"] as? NSNumber)?.intValue
age = (dict["age"] as? NSNumber)?.intValue
}
}
MVVM层:PersonRxViewModel.swift
import UIKit
import RxSwift
/*
1、ViewModel层:负责请求数据
2、ViewModel层:负责处理数据
3、ViewModel层:负责存储数据
*/
class PersonRxViewModel: NSObject {
// 持有一个_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 publishPersonVMArray = 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 PersonRxViewModel {
/// 请求数据
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事件把数据发出去
publishPersonVMArray.onNext(_personVMArray)
//-----------变化2-----------//
}
}
view层:PersonTableViewCell.swift
class PersonTableViewCell: UITableViewCell {
@IBOutlet weak var sexLabel: UILabel!
@IBOutlet weak var ageName: UILabel!
@IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
var personVM: PersonViewModel? {
didSet {
guard let personVM = personVM else { return }
nameLabel.text = personVM.name
sexLabel.text = personVM.sex
ageName.text = "\(personVM.age)"
}
}
}
Controller层:PersonRxMVVMController.swift
import UIKit
import SnapKit
import RxSwift
import RxCocoa
class PersonRxMVVMController:UIViewController {
let bag = DisposeBag()
private lazy var _textField: UITextField = {
let textField = UITextField()
textField.backgroundColor = UIColor.red
textField.returnKeyType = .done
return textField
}()
lazy var tableView: UITableView = {
let tableView = UITableView.init(frame: .zero, style: .grouped)
tableView.backgroundColor = .white
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
tableView.register(UINib(nibName: "PersonTableViewCell", bundle: nil), forCellReuseIdentifier: "cellId")
return tableView
}()
private lazy var _personVM = PersonRxViewModel()
private var _insertText = "张";
override func viewDidLoad() {
super.viewDidLoad()
_addViews()
_layoutViews()
// 再把数据和UI进行双向绑定
_setupRxSwift()
// 初始请求数据
_loadData(params: "person")
}
}
extension PersonRxMVVMController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
// MARK: - setupRxSwift
extension PersonRxMVVMController {
private func _setupRxSwift() {
// 数据绑定到tableView上:数据驱动UI
_personVM.publishPersonVMArray.bind(to: tableView.rx.items) {
tableView, indexPathRow, data in
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: IndexPath(row: indexPathRow, section: 0)) as! PersonTableViewCell
cell.personVM = data
return cell
}
.disposed(by: bag)
tableView.rx.setDelegate(self).disposed(by: bag)
// textField的内容发生变化后,重新请求数据:UI驱动数据
_textField.rx.controlEvent(.editingDidEnd).subscribe { _ in
// textField结束编辑了、光标停止闪动
print(self._textField.text ?? "")
// 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)
}
}
// MARK: - 请求数据
extension PersonRxMVVMController {
private func _loadData(params: String) {
_personVM.loadData(params: params) { isSuccess in
if !isSuccess {
print("请求数据出错")
}
}
}
}
// MARK: - setupUI
extension PersonRxMVVMController {
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.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(_textField.bottom).offset(0)
make.bottom.equalTo(view).offset(-SafeBottomHeight-44)
}
//tableView.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
}
}