扩展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组件的更多相关文章

  1. React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发

    React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发   2016/09/23 |  React Native技术文章 |  Sky丶清|  4 条评论 |  1 ...

  2. 如何发布自定义的UI 组件库到 npmjs.com 并且编写 UI组件说明文档

    记录基于 antd 封装业务组件并发布到npm 上的过程:(TS + React + Sass) 初始化项目: 1.yarn create react-app winyhui --typescript ...

  3. Wuss Weapp 一款高质量,组件齐全,高自定义的微信小程序 UI 组件库

    Wuss Weapp 一款高质量,组件齐全,高自定义的微信小程序 UI 组件库 文档 https://phonycode.github.io/wuss-weapp 扫码体验 使用微信扫一扫体验小程序组 ...

  4. vue+ElementUI+高德API地址模糊搜索(自定义UI组件)

    开发环境描述: Vue.js ElementUI 高德地图API 需求描述: 在新增地址信息的时候,我们需要根据input输入的关键字调用地图的输入提示API,获取到返回的数据,并根据这些数据生成下拉 ...

  5. iOS(Swift)学习笔记之SnapKit+自定义UI组件

    本文为原创文章,转载请标明出处 1. 通过CocoaPods安装SnapKit platform :ios, '10.0' target '<Your Target Name>' do u ...

  6. AngularJs的UI组件ui-Bootstrap分享(八)——Tooltip和Popover

    tooltip和popover是轻量的.可扩展的.用于提示的指令.对于移动端来讲,这两个指令虽然可以正常工作,但是从用户体验的角度并不推荐使用. 先说tooltip,tooltip有三种使用方式: ( ...

  7. AngularJs的UI组件ui-Bootstrap分享(七)——Buttons和Dropdown

    在ui-Bootstrap中,Buttons控件和Dropdown控件与form表单中的按钮和下拉框名字很像,但实际上这两个控件有新的含义. 先说Buttons,它是一组按钮,用来实现form表单中的 ...

  8. AngularJs的UI组件ui-Bootstrap分享(四)——Datepicker Popup

    Datepicker Popup是用来选择日期的控件,一般和文本框一起使用,功能和Jquery的插件My97DatePicker一样.在Datepicker Popup内部使用了ui-bootstra ...

  9. 基于 HtmlHelper 的自定义扩展Container

    基于 HtmlHelper 的自定义扩展Container Intro 基于 asp.net mvc 的权限控制系统的一部分,适用于对UI层数据呈现的控制,基于 HtmlHelper 的扩展组件 Co ...

  10. 目前比较火的前端框架及UI组件

    看到的一篇总结性的文章,收藏一下,感兴趣的可以自己看看,哪些是已经会的,哪些是没听说过的,哪些是一知半解的,都可以稍微看看. 一.前端框架库: 1.Zepto.js 地址:点击打开链接 描述:Zept ...

随机推荐

  1. NETAPP硬盘更换

    netapp硬盘新增 一.找到坏盘,插上新盘# 1.登陆到想要点亮的硬盘相对应的控制器上,并进去高级模式. priv set advanced 2.利用disk show -v 查看想要点亮的硬盘名字 ...

  2. JZOJ 3281. 【GDOI2013】字母连接

    \(\text{Solution}\) 一眼不会,限制有点多... 那就网络流 发下确实是很简单的建图 枚举起点集合 拆点后就很好满足限制了 \(\text{Code}\) #include < ...

  3. Android中drawable和mipmap到底有什么区别

    欢迎通过我的个人博客来查看此文章 老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这 ...

  4. 使用vscode编辑markdown

    目录 markdown在vscode中的使用 标题 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 列表 图片 表格 网址 代码 文本样式 引用 目录 vscode中使用的插件推荐 截图工 ...

  5. Mars3D与第三方集成

    1. 引言 Mars3D是基于Cesium的Web端的三维GIS库,对Cesium做了进一步封装和扩展 Mars3D官网:Mars3D三维可视化平台 | 火星科技 Mars3D开发手册:开发教程 - ...

  6. string str = string.Empty也会出错?

    如题 为什么会出现这种情况?大佬解释一下.

  7. Django中models的字段

    常见的field类型: 1.AutoField 自增字段,它是一个根据ID自增长的IntegerField字段,通常不用自己设置,如果没有设置主键,django会自动添加它为主键字段 2.CharFi ...

  8. Spring cloud Alibaba Nacos服务注册发现和配置中心

    Nacos(官方网站:http://nacos.io)是一个易于使用的平台,旨在用于动态服务发现,配置和服务管理.它可以帮助您轻松构建云本机应用程序和微服务平台. Nacos = Eureka + c ...

  9. python基本语法入门

    思维导图 https://gitee.com/starry-tong/python-data/blob/pyimage/day03.png python语法注释 """注 ...

  10. Kotlin相关语法

    1.Kotlin的匿名函数 { val  a = 1 val b = 2 a+b } 就是一个不带名字的函数体 2.Kotlin的函数类型 函数类型:用来声明一个函数参数和返回值形式的 特殊数据类型声 ...