前言
经过对RxSwift的工作原理简单分析后iOS框架 - RXSwift原理初探,我们就来看看如何使用及分析下对应的工作流程。
RxSwift版本-6.5.0
1、准备工作
新建Swift项目,利用Cocoapods集成RxSwift和RxCocoa两个三方库:
platform :ios, '10.0'
target 'Rx-Test1' do
use_frameworks!
pod 'RxSwift'
pod 'RxCocoa'
end
2、KVO
新建Person类,添加属性name,点击页面改变name以观察name属性的变动:
class Person: NSObject {
var name: String = "Lcr"
}
在ViewController添加如下代码:
var person: Person = Person()
let disposeBag = DisposeBag()
func setupKVO() {
self.person.rx.observe(String.self, "name")
.subscribe { value in
print(value as Any)
}
.disposed(by: disposeBag)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.person.name = "xuxixixixiixi"
}
调用setupKVO(),当我们点击屏幕时,改变person.name,发现报如下错误:
Thread 1: "[<Rx_Test1.Person 0x60000374ef80> addObserver:<_TtC7RxCocoaP33_F7515DBB13B60709A3CB25DD19EDD11D11KVOObserver 0x600003907270> forKeyPath:@"name" options:5 context:0x0] was sent to an object that is not KVC-compliant for the "name" property."
也就是说不能通过KVC方式去获取到name属性,也就是获取不到name属性,所以需要给name属性添加上@objc
修饰符:
@objc var name: String = "Lcr"
重新运行立即打印结果:next(Optional("Lcr"))
,点击屏幕打印next(Optional("xuxixixixiixi"))
,说明当name值改变的时候就会触发一次.next
方法。
查看.observe方法定义:
![](https://img.haomeiwen.com/i4469396/1a9e55d5c4f9843e.png)
返回KVOObservable类型的序列,即.observe生成一个序列,初始化只是简单保存相应的参数:
![](https://img.haomeiwen.com/i4469396/8cf78e6553824d7c.png)
接着.subscribe
方法的调用(KVOObservable内部的.subscribe),
![](https://img.haomeiwen.com/i4469396/b843c5d61f4d9a48.png)
发现里面生成一个KVOObserver类型的observer观察者,进入初始化方法,然后继续super.init方法,就会发现_RXKVOObserver
类内的代码就很熟悉了:
![](https://img.haomeiwen.com/i4469396/1867c1f163e84a35.png)
![](https://img.haomeiwen.com/i4469396/cd3f79f292ead7bb.png)
实现了addObserver、observeValueForKeyPath以及removeObserver
三部曲,self.target为person对象,self.keyPath为"name",self.callback
为下图中的闭包即KVOObserver初始化带进来的:
![](https://img.haomeiwen.com/i4469396/92a358695b3a3d95.png)
所以当name属性值改变时,
self.callback(change[NSKeyValueChangeNewKey])
就会触发一次闭包运行,接着observer.on(.next))
,最后就会运行到最外层订阅的闭包,并将新值作为参数带进去:
.subscribe { value in
print(value as Any)
}
可见RxSwift内部实现中间类为我们做了我们想做的事,代码量少,以及代码都在一块,方便管理。
3、UIButton点击
func setButtonTap() {
//self.button.rx.tap 也可直接这么用因为 tap = controlEvent(.touchUpInside)
self.button.rx.controlEvent(.touchUpInside)
.subscribe { _ in
print("点击按钮")
}
.disposed(by: disposeBag)
}
也可直接.tap方式添加按钮的点击事件,查看.tap即为controlEvent(.touchUpInside)
。
查看.controlEvent方法实现:
![](https://img.haomeiwen.com/i4469396/7cd20408848ce52a.png)
同样的也是返回一个ControlEvent
类型的序列,内部也初始化了一个观察者source,并以这个source为参数进行初始化:
![](https://img.haomeiwen.com/i4469396/3997540a81cee3bd.png)
发现self.events为外界的source序列通过.subscribe函数返回的SubscribeOn类型的序列:
![](https://img.haomeiwen.com/i4469396/d33c4407c6c7835f.png)
此处的source还是为外界带进来的source序列,scheduler为调度者:
![](https://img.haomeiwen.com/i4469396/ff8b4dc521291d36.png)
查看
SubscribeOn
类包括初始化方法及run方法,看到这里是不是感觉这个类的结构如此熟悉?没错!可以直接来看
sink.run()
方法:![](https://img.haomeiwen.com/i4469396/b7767010fe7834b1.png)
此处的source即为最外层.controlEvent方法中创建的source序列,触发.subscribe后,.controlEvent中的闭包就会运行,于是
![](https://img.haomeiwen.com/i4469396/7cd20408848ce52a.png)
就来到ControlTarget初始化中:
从中可以看出,这是给Button添加点击事件,control.addTarget(self, action: selector, for: controlEvents)
。
4、UITextField
输入框主要就是监听输入事件以及输入后的响应:
func setupTextfield() {
self.textfield.rx.text.orEmpty.changed
.subscribe { text in
print(text)
}.disposed(by: disposeBag)
}
查看.text
里到底做了什么:
![](https://img.haomeiwen.com/i4469396/02322c17ef487f43.png)
继续查看
base.rx.controlPropertyWithDefaultEvents
,![](https://img.haomeiwen.com/i4469396/c6fd083a938a0d97.png)
go on,相关重要操作就在
.controlProperty
函数里面:![](https://img.haomeiwen.com/i4469396/90082e9152ddebdb.png)
进入ControlProperty初始化函数,发现似曾相识的values.subscribe(on:
![](https://img.haomeiwen.com/i4469396/de06531482905407.png)
![](https://img.haomeiwen.com/i4469396/5918d20bfe605346.png)
类似于按钮的点击,接下里的步骤都是一样的,最终都会回到上方.text函数中的闭包调用,即给输入框添加事件响应。
control.addTarget(self, action: selector, for: controlEvents)
5、手势
func setupGestureRecognizer() {
let tap = UITapGestureRecognizer()
self.textLcbel.addGestureRecognizer(tap)
self.textLcbel.isUserInteractionEnabled = true
tap.rx.event.subscribe { tap in
print(tap.view!)
}.disposed(by: disposeBag)
}
查看.event方法:
![](https://img.haomeiwen.com/i4469396/4047ec44225da736.png)
6、通知
func setupNotification() {
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe { noti in
print(noti)
}.disposed(by: disposeBag)
}
查看.notification函数:
![](https://img.haomeiwen.com/i4469396/9ab0a083d53ca1b1.png)
当.subscribe订阅之后,必定会来到上图中的闭包,闭包运行,实际就是添加通知以及响应闭包。当收到通知时,再运行保存的闭包:
{ notification in
observer.on(.next(notification))
}
就能触发.subscribe后的闭包运行,处理相关事件,达到接收通知的目的。
7、定时器
拓展:
Swift三种定时器对比:
var timer = Timer()
var gcdTimer: DispatchSourceTimer?
var cadTimer: CADisplayLink?
//方式一 NSTimer
timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
timer.fire()
RunLoop.current.add(timer, forMode: .common)
//方式二 GCD
gcdTimer = DispatchSource.makeTimerSource()
gcdTimer?.schedule(deadline: .now(), repeating: .seconds(1))
gcdTimer?.setEventHandler(handler: {
print("hello GCD")
})
gcdTimer?.resume()
// gcdTimer?.suspend()
// gcdTimer?.cancel()
// gcdTimer = nil
//方式三 CADisplayLink
cadTimer = CADisplayLink(target: self, selector: #selector(timerFire))
cadTimer?.preferredFramesPerSecond = 1
cadTimer?.add(to: .current, forMode: .common)
@objc func timerFire() {
print("Lcr111")
}
三种方式都可实现每秒打印的需求,但是NSTimer和CADisplayLink需要添加进RunLoop
中,而且还得是 .common
模式,不然会被UI事件影响正常的运行。
那RxSwift中使用的是哪一种方案呢?
在我们平时开发中,GCD用的比较多,使用方便,更精确,操作性也更强,且不用依赖RunLoop,不受UI事件影响,那RxSwift中是否也是用的GCD的方式呢?
Observable<Int>.timer(.seconds(1), period: .seconds(1), scheduler: MainScheduler.instance)
.subscribe { result in
print(result)
}
.disposed(by: disposeBag)
查看.timer里的实现:
![](https://img.haomeiwen.com/i4469396/c0baf159046db9f6.png)
![](https://img.haomeiwen.com/i4469396/6440c83f2c4bdf1e.png)
![](https://img.haomeiwen.com/i4469396/664becd74fc04539.png)
工作模式都是一样的,所以我们直接来到
run
方法中,继续跟进:![](https://img.haomeiwen.com/i4469396/884175d8bf58cf33.png)
查看并行队列调度者中的实现:
![](https://img.haomeiwen.com/i4469396/081c95f0723f56fa.png)
![](https://img.haomeiwen.com/i4469396/acc562d8414f8ead.png)
DispatchSource.makeTimerSource(queue: self.queue)
,可见RxSwift定时器就是封装的GCD
,
timer.setEventHandler(handler: {
if cancelTimer.isDisposed {
return
}
timerState = action(timerState)
})
任务闭包中action就是run()
方法中带进来的闭包,
{ state in
self.lock.performLocked {
self.forwardOn(.next(state))
return state &+ 1
}
这个闭包中的forwardOn最终执行的就是外界的onNext
事件。state在这个闭包内每秒增加1,这样外面打印都是递增的数值。
注意:
- 当第二个参数period没有没有给的时候,此时的定时器任务
只执行一次
。 - 也可使用
interval
方式创建定时器,而且可以不指定period
参数也可无限制
执行任务。
8、网络请求
func setupNetwork() {
let url = URL(string: "https://www.baidu.com")
//方式一
// URLSession.shared.dataTask(with: url!) { data, response, error in
// print(String.init(data: data!, encoding: .utf8)!)
// }.resume()
//方式二
URLSession.shared.rx.response(request: URLRequest(url: url!))
.subscribe { response, data in
print(response)
}
.disposed(by: disposeBag)
}
查看.response:
![](https://img.haomeiwen.com/i4469396/88dfc97ca103e013.png)
方式二中RxSwift就是将
.dataTask
请求任务封装在内部,外部返回的数据中将各种情况分开处理,成功、失败和完成以及销毁都是分开处理的,不像方式一中处理返回结果时还得判断是否出错等等。
9、简单示例
下面例子就是日常开发中,登录界面很正常的功能,账号和密码长度的校验功能。
1.用户名长度不小于5,小于5时候密码不可输入
。
2.密码长度不小于5
。
3.登录按钮在满足上面两条件下才能点击
。
let nameValid = textfield.rx.text.orEmpty
.map { text in
return text.count >= 5
}
nameValid.bind(to: textLcbel.rx.isHidden)
.disposed(by: disposeBag)
nameValid.bind(to: passwordTf.rx.isEnabled)
.disposed(by: disposeBag)
// passwordTf 密码输入框
// passwordLb 密码提示语
let passwValid = passwordTf.rx.text.orEmpty
.map { text in
return text.count >= 5
}
passwValid.bind(to: passwordLb.rx.isHidden)
.disposed(by: disposeBag)
// button 登录按钮
Observable.combineLatest(nameValid, passwValid){$0 && $1}
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
button.rx.tap.subscribe { _ in
print("登录")
}.disposed(by: disposeBag)
.combineLatest
我们下篇文章会详细介绍,此处就是达到同时订阅两个序列的目的,同时监听两个条件的满足与否。
本文只是对一些常用的UI控件对应RxSwift的简单使用以及原理探究,接下来会接触更多使用及原理探究。
网友评论