RxCocoa
RxCocoa适用于所有平台,针对每个平台的需求:iOS、watchOS、iPadOS、tvOS、macOS和Mac Catalyst。每个平台都有一组自定义包装器,为许多UI控件和其他SDK类提供一组内置扩展。
下文中即将用到的UITextField和UITextView都将使用RxCocoa
UITextField和UITextView
UITextField的问题
对UITextField赋值
先来看代码
func testTextFieldAndTextView () {
_ = textFiled.rx.text.subscribe(onNext: { text in
guard let str = text else { return }
print("textField 收到\(str)")
})
_ = textView.rx.text.subscribe(onNext: { text in
guard let str = text else { return }
print("textView 收到\(str)")
})
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("12345")
textFiled.text = "field-123456"
textView.text += "-123"
}
打印结果:
textField 收到
textView 收到view-123456
可以看到在初始化的时候二者都会有一次订阅的打印,但是在点击屏幕后,在对UITextField和UITextView的赋值后,仅有UITextView有订阅的打印,而UITextField虽然控件也看到了赋值,但是并无订阅的打印
那么是否此框架出现了问题?
这时先使用一个常规方法来验证一下
textFiled.addTarget(self, action: #selector(textFiledChange), for: .allEditingEvents)
同样在点击屏幕后虽然还是给UITextField进行了赋值,但是textFiledChange并未响应,这里说明刚才的问题并不是出了bug,原因是因为赋值的操作并不是属于control event事件,如果只是赋值操作之后可以通过方法sendActionsForControlEvents手动的进行通知
所以如果对UITextField进行赋值但同时也想得到回调,则可以使用方法sendActionsForControlEvents,如下即可
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("12345")
textFiled.text = "field-123456"
textView.text += "-123"
textFiled.sendActions(for: .allEditingEvents)
}
两次订阅(初始化订阅和点击控件的订阅)
在初始化的时候和点击控件的时候都会各有一次的订阅事件,那么现在来看看这两次的调用都是怎么来的
从上图可以看出text是ControlProperty类型,最终返回的是value,重点应该关注到controlPropertyWithDefaultEvents
最终调用到上图的函数中,这里会将最终创建的ControlProperty类型序列返回出去,接下来是外边对这个队列进行订阅subscribe来触发的
回调用会来到下图所示这里
第一次的发送就是这里observer.on(.next(getter(control))),此时textField的text为空,所以第一次会发送一个空值出去
同时往下看,其实会给textField添加一个target-action事件,只要触发allEditingEvents 或者valueChanged中的一个事件,便会调用eventHandler,在eventHandler中其实调用的就是创建ControlTarget时的尾随闭包
也就是说一旦点击了textField就会使其调用发送信号的命令。
以上即为UITextField在初始化和在点击控件时产生的两次订阅
UITextView
不同于UITextField,UITextView在赋值的时候同样可以拿到订阅,因为其内部的实现是用通知来完成的
Driver
访问网络时可能发生的问题
func normalNetworkObservableUse() {
let result = inputTF.rx.text.skip(1).flatMap { input in
return self.dealWithData(inputText: input!)
}
_ = result.subscribe(onNext: { element in
print("第一次订阅到:\(element) ----- \(Thread.current)")
})
_ = result.subscribe(onNext: { element in
print("第二次订阅到:\(element) ----- \(Thread.current)")
})
}
func dealWithData(inputText: String)-> Observable<Any> {
print("请求网络:\(Thread.current)")
return Observable<Any>.create({ ob -> Disposable in
if inputText == "1234" {
ob.onError(NSError.init(domain: "com.google.cn", code: 10086, userInfo: nil))
}
DispatchQueue.global().async {
print("发送之前:\(Thread.current)")
ob.onNext("已经输入:\(inputText)")
ob.onCompleted()
}
return Disposables.create()
})
}
上述代码是在模仿一个网络请求的场景,打印结果如下:
从中可以发现三个问题:
- 每一次订阅都会访问一次网络,造成浪费流量
- 回调回来后可能会造成子线程更新
UI - 无法处理发送的错误事件
那么针对以上三个问题在此代码中要如何修改?
请求多次网络
在请求多次网络这个问题上可以如下修改,这样可以保证状态共享从而使得网络请求只有一次,哪怕是多次订阅
let result = inputTF.rx.text.skip(1).flatMap { input in
return self.dealWithData(inputText: input!)
}.share(replay: 1, scope: .whileConnected)
回调的环境处于子线程
这样使得序列会包装源序列以便在指定的调度调度运行它的观察者回调,在此例中便是可以在主线程进行回调
let result = inputTF.rx.text.skip(1).flatMap { input in
return self.dealWithData(inputText: input!).observe(on: MainScheduler.instance)
}.share(replay: 1, scope: .whileConnected)
错误问题处理
由于无法直接将错误返回给UI来进行处理,在使用此方法后可以将错误转化为字符串类型来处理
let result = inputTF.rx.text.skip(1).flatMap { input in
return self.dealWithData(inputText: input!)
.observe(on: MainScheduler.instance)
.catchAndReturn("捕捉到错误")
}.share(replay: 1, scope: .whileConnected)
使用Driver
上面的代码完全可以用Driver来替换完成以上功能
let result = inputTF.rx.text.asDriver().flatMap { input in
return self.dealWithData(inputText: input!).asDriver(onErrorJustReturn: "监测到错误事件")
}
_ = result.map { "长度:\(($0 as! String).count)" }.drive(self.textLabel.rx.text)
_ = result.map { "\($0)" }.drive(self.btn.rx.title())
在上图的方法中可以看到observe(on:DriverSharingStrategy.scheduler)其实相当于之前例子中的observe(on: MainScheduler.instance),因为看到内部既是如此,所以这里相当于在主线程来调度的意思
catchAndReturn(onErrorJustReturn)就是之前的错误处理
而共享状态则是被封装在了内部