rxswift自定义扩展UI组件
扩展UI组件时常用到的一些发布者与订阅者如下:
发布者:
ControlEvent(专门用于描述 UI 控件所产生的事件)
订阅者(观察者):
- Binder(专门用于绑定UI状态的,如:当某个状态改变时,更新UI状态)
既是发布者又是订阅者:
ControlProperty(专门用于描述 UI 控件属性的,如:为属性赋值、订阅属性值变化)
为UI控件扩展RX能力时,一般都是对Reactive进行扩展,如下:为UIView扩展backgroundColor状态绑定
extension Reactive where Base: UIView {
var backgroundColor: Binder<UIColor?> {
Binder(base){ (v: UIView, color: UIColor?) in
v.backgroundColor = color
}
}
}
这里采用的是swift的条件扩展,Base是Reactive的泛型类型,当Base为UIView时,为Reactive扩展backgroundColor计算属性,属性类型为Binder(观察者,用于观察外部状态变化,从而更新视图背景色)
目前Reactive内部实现了动态成员查找,我们不再需要为类的属性分别扩展状态绑定,先看一下Reactive内部是如何实现的:
@dynamicMemberLookup
public struct Reactive<Base> {
/// Base object to extend.
public let base: Base /// Creates extensions with base object.
///
/// - parameter base: Base object.
public init(_ base: Base) {
self.base = base
} /// Automatically synthesized binder for a key path between the reactive
/// base and one of its properties
public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
Binder(self.base) { base, value in
base[keyPath: keyPath] = value
}
}
}
关于动态成员查找@dynamicMemberLookup的描述,可以参考这篇文章:@dynamicMemberLookup(动态成员查找)
UIControl视图组件的扩展
RxCocoa为我们扩展了UIControl,它提供了以下两个方法:
A public func controlEvent(_ controlEvents: UIControl.Event) -> ControlEvent<()> B public func controlProperty<T>(
editingEvents: UIControl.Event,
getter: @escaping (Base) -> T,
setter: @escaping (Base, T) -> Void
) -> ControlProperty<T>
方法A返回一个ControlEvent类型的发布者,它专门用于发出UIControl触发的事件,如点击事件。
方法B返回一个ControlProperty类型的(发布者&订阅者),它既可以用于发出UIControl触发的事件,又可以绑定状态变化,从而改变UIControl的状态。
因此有了上面的两个方法,我们就可以轻松实现UIControl类型视图的事件处理与状态绑定。
下面是RxCocoa为我们实现的UIButton的tap事件发布者:
extension Reactive where Base: UIButton { /// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
有时有的视图组件某个属性既需要绑定状态,又需要可以发出某些事件的情况,针对这种情况我们就需要用到ControlProperty类型来扩展UI属性。
下面是一个UITextField扩展text属性的实现,它是RxCocoa提供的:
extension Reactive where Base: UITextField {
/// Reactive wrapper for `text` property.
public var text: ControlProperty<String?> {
value
} /// Reactive wrapper for `text` property.
public var value: ControlProperty<String?> {
return base.rx.controlPropertyWithDefaultEvents(
getter: { textField in
textField.text
},
setter: { textField, value in
// This check is important because setting text value always clears control state
// including marked text selection which is important for proper input
// when IME input method is used.
if textField.text != value {
textField.text = value
}
}
)
} /// Bindable sink for `attributedText` property.
public var attributedText: ControlProperty<NSAttributedString?> {
return base.rx.controlPropertyWithDefaultEvents(
getter: { textField in
textField.attributedText
},
setter: { textField, value in
// This check is important because setting text value always clears control state
// including marked text selection which is important for proper input
// when IME input method is used.
if textField.attributedText != value {
textField.attributedText = value
}
}
)
}
}
因为UITextField继承自UIControl,因此我们可以利用之前对UIControl扩展的方法,快速为UITextField添加text:ControlProperty属性。controlPropertyWithDefaultEvents:就是UIControl扩展提供的方法,它默认指定的controlEvents为:[.allEditingEvents, .valueChanged],即:当UITextField触发所有编辑事件和值发生改变时,会对外发出这些事件。
RxCocoa为我们还提供了以下继承自UIControl的UI组件:
- UITextFiled.text、UITextField.value、UITextField.attributedText(ControlProperty类型)
- UITextView.text、UITextView.value、UITextView.attributedText(ControlProperty类型)
UISlider.value(ControlProperty类型)
UIStepper.value(ControlProperty类型)
UISwitch.isOn、UISwitch.value(ControlProperty类型)
UIDatePicker.date、UIDatePicker.value、UIDatePicker.countDownDuration(ControlProperty类型)
UISegmentedControl.selectedSegmentIndex、UISegmentedControl.value(ControlProperty类型)
Selector方法Hook扩展
有时我们需要对UI组件的某个方法调用增加监听,例如监听UIViewController的viewDidLoad方法被调用后,加载页面数据。RxCocoa为我们提供了两个方法,来实现对实例方法的hook:
public func sentMessage(_ selector: Selector) -> Observable<[Any]> public func methodInvoked(_ selector: Selector) -> Observable<[Any]>
这两个方法唯一的区别就是,sentMessage会在selector方法调用前,发出消息,消息内容为selector方法的参数;而methodInvoked会在selector方法调用后,发出消息,消息内容为selector方法参数。
这两个方法返回Observable<[Any]>,我们可以对它进行订阅,从而在方法selector调用前后,处理一些逻辑。
使用这两个方法对自定义的方法进行Hook时需要注意、注意、注意:自定义的方法前必须使用@objc dynamic标注,否则Hook无效。这里与KVO的使用注意项类似
这里简单举个例子:
@objc dynamic // 这里是必须的
private func login(name: String, pwd: String) -> Bool {
guard name == "aaa", pwd == "bbb" else {
return false
}
return true
} // Hook
rx.sentMessage(#selector(login(name:pwd:))).map({$0.map(String.init(describing:))}).bind { (params: [String]) in
print("call login before, userName: \(params[0]), password: \(params[1])")
}.disposed(by: disposeBag)
这里推荐一个UIViewController的rx扩展,它提供了viewDidLoad、viewWillAppear等方法的Hook:RxViewController
对于处理方法Hook的发布者我们一般采用ControlEvent,因此RxViewController.viewDidLoad的返回值类型为:ControlEvent,我们可以看一下具体是如何扩展一个viewDidLoad的:
extension Reactive where Base: UIViewController { var viewDidLoad: ControlEvent<Void> {
// 这里采用methodInvoked,意味着会在viewDidLoad方法调用后,发出事件消息。如果你需要在ViewDidLoad方法调用前发出消息,你可以选择使用sentMessage实现
let obsed = self.methodInvoked(#selector(Base.viewDidLoad)).map { _ in}
return ControlEvent<Void>(events: obsed)
} }
Delegate的方法Hook扩展
RxCocoa实现Delegate的hook的核心原理,看下面的一张图就很容易理解:
RxCocoa通过代理的代理,实现方法转发,将实际的调用者转发给forwardToDelegate,并且实现了在forwardToDelegate调用方法前后增加了_sentMessage:withArguments:和_methodInvoked:withArguments:的调用。(注意:只有当前的方法返回值为void时,这两个方法才会调用)
RxCocoa提供一个DelegateProxy类,它继承自_RXDelegateProxy,因此DelegateProxy具有代理转发功能,即:代理的代理。
DelegateProxy它重写了上面两个方法:
- _sentMessage:withArguments:
- _methodInvoked:withArguments:
这两个方法会通过selector获取对应要触发的绑定函数,这些函数存储在当前类DelegateProxy的以下属性中:
- _sentMessageForSelector:[Selector:MessageDispatcher]
- _methodInvokedForSelector:[Selector:MessageDispatcher]
并且DelegateProxy还提供如下方法来为代理方法添加绑定函数,将绑定函数保存在以上两个属性中:
- sentMessage(_ selector: Selector) -> Observable<[Any]>
- methodInvoked(_ selector: Selector) -> Observable<[Any]>
一般为代理添加RX扩展,就会用到上面这两个方法。
另外还有一个非常重要的协议:DelegateProxyType,这个协议用于注册代理的代理,它提供了如下两个方法:
- static func register<Parent>(make: @escaping (Parent) -> Self)
- static func proxy(for object: ParentObject) -> Self
register:用于初始化代理的代理,并保存在sharedFactory中
proxy:用于获取上面注册的代理的代理
因此要实现自定义Delegate的rx扩展,你需要如下几个步骤:
- 定义个代理的代理类,使之继承DelegateProxy。例如:CustomDelegateProxy:DelegateProxy
- 让上面的代理的代理实现自定义协议,如果代理的方法是可选的,你可以不用实现。例如:CustomDelegateProxy:CustomDelegate
- 让上面的代理的代理实现DelegateProxyType协议,实现注册代理类方法,以及设置原代理方法。
- 在CustomDelegateProxy中通过sentMessage或methodInvoked定义相关方法的RX扩展即可
这里举个例子:
// 代理
@objc
protocol CustomActionSheetDelegate: NSObjectProtocol { func didSelectItem(item: String)
@objc optional func didCancel()
} // 代理的代理类
class CustomActionSheetDelegateProxy:
DelegateProxy<CustomActionSheet, CustomActionSheetDelegate>,
DelegateProxyType,
CustomActionSheetDelegate{ private(set) var actionSheet: CustomActionSheet? init(actionSheet: CustomActionSheet) {
self.actionSheet = actionSheet
super.init(parentObject: actionSheet, delegateProxy: CustomActionSheetDelegateProxy.self)
} static func registerKnownImplementations() {
// 注册代理类
register(make: {CustomActionSheetDelegateProxy(actionSheet: $0)})
} static func currentDelegate(for object: CustomActionSheet) -> CustomActionSheetDelegate? {
object.delegate
} static func setCurrentDelegate(_ delegate: CustomActionSheetDelegate?, to object: CustomActionSheet) {
object.delegate = delegate
} private var _selectItemSubject: PublishSubject<String>?
var innerSelectItemSubject: PublishSubject<String> {
if let sub = _selectItemSubject {
return sub
}
let sub = PublishSubject<String>()
_selectItemSubject = sub
return sub
} // 实现CustomActionSheetDelegate方法
// 由于该方法被实现,因此执行代理方法时,不会走方法转发,因此当设置了原代理时
// 原代理的方法也就不会被执行,因此下面方法增加了对forwardToDelegate的判断
func didSelectItem(item: String) {
if let delegate = forwardToDelegate(), delegate.responds(to: #selector(didSelectItem(item:))) {
delegate.didSelectItem(item: item)
}
innerSelectItemSubject.onNext(item) // 发送选择消息
} deinit{
if let sub = _selectItemSubject {
sub.on(.completed) // 结束订阅
}
} }
添加Reactive扩展:
// 扩展RX
extension Reactive where Base: CustomActionSheet { // 对外提供代理的代理
var delegate: DelegateProxy<CustomActionSheet, CustomActionSheetDelegate>{
CustomActionSheetDelegateProxy.proxy(for: base)
} // 设置原代理的方法
func setDelegate(_ delegate: CustomActionSheetDelegate) -> Disposable {
CustomActionSheetDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: base)
} // 新增选择rx扩展
var didSelectItem: ControlEvent<String> {
// 因为代理的代理类,实现了didSelectItem方法,因此这里不能通过delegate.methodInvoked进行绑定
// delegate.methodInvoked(#selector(CustomActionSheetDelegate.didSelectItem(item:)))
// 需要如下方式:
let source = CustomActionSheetDelegateProxy.proxy(for: base).innerSelectItemSubject
return ControlEvent(events: source)
} // 新增取消rx扩展
var cancel: ControlEvent<Void> {
let source = delegate.methodInvoked(#selector(CustomActionSheetDelegate.didCancel)).map({_ in})
return ControlEvent(events: source)
}
}
以上只是处理一些没有返回值的代理方法,上面提到过,当代理方法有返回值的时候,sentMessage和methodInvoked无法正确执行,因此我们无法通过这两个方法添加绑定。
一般带有返回值的代理我们叫它DataSource,在RxCocoa中实现DataSource原理如下:
- 创建一个DataSource的实现类,例如:RxTableViewReactiveArrayDataSource: UITableViewDataSource
- 为实现类添加初始化方法,参数:一个用于返回TableViewCell的闭包函数
- 在实现类中增加Items,用于存储数据源
- 实现类再次实现RxTableViewDataSourceType协议
- 在Reactive扩展中,新增items函数,参数为:RXTableViewDataSource & UITableViewDataSource,返回值为函数:(ObservableType) -> Disposable,它可以用于bind(to:)函数绑定。
我们看一下items的函数:
public func items<
DataSource: RxTableViewDataSourceType & UITableViewDataSource,
Source: ObservableType>
(dataSource: DataSource)
-> (_ source: Source)
-> Disposable
where DataSource.Element == Source.Element {
return { source in
// This is called for side effects only, and to make sure delegate proxy is in place when
// data source is being bound.
// This is needed because theoretically the data source subscription itself might
// call `self.rx.delegate`. If that happens, it might cause weird side effects since
// setting data source will set delegate, and UITableView might get into a weird state.
// Therefore it's better to set delegate proxy first, just to be sure.
_ = self.delegate
// Strong reference is needed because data source is in use until result subscription is disposed
return source.subscribeProxyDataSource(ofObject: self.base, dataSource: dataSource as UITableViewDataSource, retainDataSource: true) { [weak tableView = self.base] (_: RxTableViewDataSourceProxy, event) -> Void in
guard let tableView = tableView else {
return
}
dataSource.tableView(tableView, observedEvent: event)
}
}
}
该方法中完成了对ObservableType的观察绑定,如:source.subscribeProxyDataSource,绑定闭包中调用了DataSource的dataSource.tableView(tableView, observedEvent: event)。
而这个方法的实现如下:
func tableView(_ tableView: UITableView, observedEvent: Event<Sequence>) {
Binder(self) { tableViewDataSource, sectionModels in
let sections = Array(sectionModels)
tableViewDataSource.tableView(tableView, observedElements: sections)
}.on(observedEvent)
}
这里再次调用了自己的tableView(tableView, observedElements: sections)函数,如下:
func tableView(_ tableView: UITableView, observedElements: [Element]) {
self.itemModels = observedElements tableView.reloadData()
}
因此当我们调用Observable.just(["first item", "second item", "third item"]).bind(to: tableView.rx.items(dataSource))时,items实现了对Observable的订阅,在订阅中调用DataSource的tableView(tableView, observedEvent: event),然后将event中的Element,即:["first item", "second item", "third item"]赋值给DataSource中的属性:items,然后调用tableView.reloadData(),然后执行DataSource中的代理回调,如下:
func numberOfSections(in tableView: UITableView) -> Int {
1
} func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
} func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let element = items[indexPath.row]
configCell(tableView, indexPath, element) // 这里是DataSource初始化时传入的闭包函数
}
这样就完成了对DataSource数据源代理的绑定,下面是具体的原理图解:
Target-Action的RX绑定
RxCocoa针对target-action进行了统一的封装,这里的target-action指的是通过addTarget:action:方法来实现UI事件绑定的方法。
例如:所有继承自UIControl,UIGestureRecognizer
RxCocoa提供了一个统一的RxTarget类,作为addTarget:action:中的target,而这个类巧妙的运用了循环引用,来控制自己的生命周期,并且它实现了Disposable协议,在dispose方法中将循环引用断开,从而释放资源。下面是RxTarget的具体定义:
class RxTarget: NSObject, Disposable { private var retainSelf: RxTarget? // 循环引用,用于控制自己的生命周期 override init() {
super.init()
self.retainSelf = self
} func dispose() {
self.retainSelf = nil // 断开循环引用
}
}
针对UIControl和UIGestureRecognizer RxCocoa提供了以下两个target类:
- ControlTarget
- GestureTarget
这两个类都继承自RxTarget,目的是为了控制自己的生命周期。并且它们在各自的初始化方法中实现了addTarget:action:的绑定,并提供一个闭包函数,来处理action的回调。
这里我们拿ControlTarget举例,以下是源码实现:
final class ControlTarget: RxTarget {
typealias Callback = (UIControl) -> Void let selector: Selector = #selector(ControlTarget.eventHandler(_:)) weak var control: UIControl? var callback: Callback?
let controlEvents: UIControl.Event init(control: UIControl, controlEvents: UIControl.Event, callback: @escaping Callback) { self.control = control
self.callback = callback
self.controlEvents = controlEvents
super.init() // 绑定target与action
control.addTarget(self, action: selector, for: controlEvents)
} @objc func eventHandler(_ sender: UIControl!) {
if let callback = self.callback, let control = self.control {
callback(control)
}
} override func dispose() { // 解除绑定
super.dispose()
self.control?.removeTarget(self, action: self.selector, for: self.controlEvents)
self.callback = nil
}
}
文章上面我们提到过UIControl视图组件的扩展,其中提供了如下方法:
public func controlEvent(_ controlEvents: UIControl.Event) -> ControlEvent<()> public func controlProperty<T>(
editingEvents: UIControl.Event,
getter: @escaping (Base) -> T,
setter: @escaping (Base, T) -> Void
) -> ControlProperty<T>
而UIGestureRecognizer提供了如下扩展:
extension Reactive where Base: UIGestureRecognizer { /// Reactive wrapper for gesture recognizer events.
public var event: ControlEvent<Base> {
let source: Observable<Base> = Observable.create { [weak control = self.base] observer in
MainScheduler.ensureRunningOnMainThread() guard let control = control else {
observer.on(.completed)
return Disposables.create()
} let observer = GestureTarget(control) { control in
observer.on(.next(control))
} return observer
}.take(until: deallocated) return ControlEvent(events: source)
} }
这样我们就可以通过如下方式绑定手势的事件:
let tap = UITapGestureRecognizer()
tap.rx.event.bind { _ in
print("tap gesture call")
}.disposed(by: disposeBag)
了解了以上RxCocoa的实现原理,我们就可以为我们自己的UI组件添加RxCocoa扩展了。RxSwift社区为我们实现了很多功能,我们可在这里查看到社区中的贡献项目。
rxswift自定义扩展UI组件的更多相关文章
- React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发
React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发 2016/09/23 | React Native技术文章 | Sky丶清| 4 条评论 | 1 ...
- 如何发布自定义的UI 组件库到 npmjs.com 并且编写 UI组件说明文档
记录基于 antd 封装业务组件并发布到npm 上的过程:(TS + React + Sass) 初始化项目: 1.yarn create react-app winyhui --typescript ...
- Wuss Weapp 一款高质量,组件齐全,高自定义的微信小程序 UI 组件库
Wuss Weapp 一款高质量,组件齐全,高自定义的微信小程序 UI 组件库 文档 https://phonycode.github.io/wuss-weapp 扫码体验 使用微信扫一扫体验小程序组 ...
- vue+ElementUI+高德API地址模糊搜索(自定义UI组件)
开发环境描述: Vue.js ElementUI 高德地图API 需求描述: 在新增地址信息的时候,我们需要根据input输入的关键字调用地图的输入提示API,获取到返回的数据,并根据这些数据生成下拉 ...
- iOS(Swift)学习笔记之SnapKit+自定义UI组件
本文为原创文章,转载请标明出处 1. 通过CocoaPods安装SnapKit platform :ios, '10.0' target '<Your Target Name>' do u ...
- AngularJs的UI组件ui-Bootstrap分享(八)——Tooltip和Popover
tooltip和popover是轻量的.可扩展的.用于提示的指令.对于移动端来讲,这两个指令虽然可以正常工作,但是从用户体验的角度并不推荐使用. 先说tooltip,tooltip有三种使用方式: ( ...
- AngularJs的UI组件ui-Bootstrap分享(七)——Buttons和Dropdown
在ui-Bootstrap中,Buttons控件和Dropdown控件与form表单中的按钮和下拉框名字很像,但实际上这两个控件有新的含义. 先说Buttons,它是一组按钮,用来实现form表单中的 ...
- AngularJs的UI组件ui-Bootstrap分享(四)——Datepicker Popup
Datepicker Popup是用来选择日期的控件,一般和文本框一起使用,功能和Jquery的插件My97DatePicker一样.在Datepicker Popup内部使用了ui-bootstra ...
- 基于 HtmlHelper 的自定义扩展Container
基于 HtmlHelper 的自定义扩展Container Intro 基于 asp.net mvc 的权限控制系统的一部分,适用于对UI层数据呈现的控制,基于 HtmlHelper 的扩展组件 Co ...
- 目前比较火的前端框架及UI组件
看到的一篇总结性的文章,收藏一下,感兴趣的可以自己看看,哪些是已经会的,哪些是没听说过的,哪些是一知半解的,都可以稍微看看. 一.前端框架库: 1.Zepto.js 地址:点击打开链接 描述:Zept ...
随机推荐
- NETAPP硬盘更换
netapp硬盘新增 一.找到坏盘,插上新盘# 1.登陆到想要点亮的硬盘相对应的控制器上,并进去高级模式. priv set advanced 2.利用disk show -v 查看想要点亮的硬盘名字 ...
- JZOJ 3281. 【GDOI2013】字母连接
\(\text{Solution}\) 一眼不会,限制有点多... 那就网络流 发下确实是很简单的建图 枚举起点集合 拆点后就很好满足限制了 \(\text{Code}\) #include < ...
- Android中drawable和mipmap到底有什么区别
欢迎通过我的个人博客来查看此文章 老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这 ...
- 使用vscode编辑markdown
目录 markdown在vscode中的使用 标题 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 列表 图片 表格 网址 代码 文本样式 引用 目录 vscode中使用的插件推荐 截图工 ...
- Mars3D与第三方集成
1. 引言 Mars3D是基于Cesium的Web端的三维GIS库,对Cesium做了进一步封装和扩展 Mars3D官网:Mars3D三维可视化平台 | 火星科技 Mars3D开发手册:开发教程 - ...
- string str = string.Empty也会出错?
如题 为什么会出现这种情况?大佬解释一下.
- Django中models的字段
常见的field类型: 1.AutoField 自增字段,它是一个根据ID自增长的IntegerField字段,通常不用自己设置,如果没有设置主键,django会自动添加它为主键字段 2.CharFi ...
- Spring cloud Alibaba Nacos服务注册发现和配置中心
Nacos(官方网站:http://nacos.io)是一个易于使用的平台,旨在用于动态服务发现,配置和服务管理.它可以帮助您轻松构建云本机应用程序和微服务平台. Nacos = Eureka + c ...
- python基本语法入门
思维导图 https://gitee.com/starry-tong/python-data/blob/pyimage/day03.png python语法注释 """注 ...
- Kotlin相关语法
1.Kotlin的匿名函数 { val a = 1 val b = 2 a+b } 就是一个不带名字的函数体 2.Kotlin的函数类型 函数类型:用来声明一个函数参数和返回值形式的 特殊数据类型声 ...