扩展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. 【HMS Core】机器学习服务助力APP快速集成图像分割与上传功能

    ​ 1.介绍 总览 机器学习服务(ML Kit)提供机器学习套件,为开发者使用机器学习能力开发各类应用,提供优质体验.得益于华为长期技术积累,ML Kit为开发者提供简单易用.服务多样.技术领先的机器 ...

  2. (原创)【B4A】一步一步入门03:APP名称、图标等信息修改

    一.前言 上篇 (原创)[B4A]一步一步入门02:可视化界面设计器.控件的使用 中我们已经了解了B4A程序的基本框架,现在我们还进一步讲解. 本篇文章会讲解如何修改APP的名称.图标等信息,以让一个 ...

  3. 【已解决】SQL2012启动时报错:cannot find one or more cpmponents

    下载Microsoft Visual Studio 2010 Shell(Isolate)-CHS安装即可 下载地址:Visual Studio 独立 Shell 下载及安装:点击同意许可,选择vs2 ...

  4. 使用brew安装历史版本的几种方式

    背景 在 mac osx 下, 大部分的软件都是使用 homebrew 进行管理的, 可以方便的进行软件的安装,更新,删除等等, 大部分情况下 homebrew 的仓库只会存在一份最新的软件版本, 有 ...

  5. 详解神经网络基础部件BN层

    摘要:在深度神经网络训练的过程中,由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称为内部协变量偏移(Internal Covariate Shift),而 BN 可以解决这个问题. 本 ...

  6. LeetCode-2104 子数组范围和

    来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/sum-of-subarray-ranges 题目描述 给你一个整数数组 nums .nums 中 ...

  7. Tensorflow 1.X 在windows上的安装

    参考:https://blog.csdn.net/weixin_42326479/article/details/105539110 pip install -i https://pypi.douba ...

  8. C语言学习--结构体指针

    #include<stdio.h> #include<string.h> //结构体指针: 指针的类型为结构体 typedef struct nodeData { int a; ...

  9. PO培训

    30M 的step by step.带notes  这个文档把常用的包含java都有 不止是配置,反正各种处理都有.. 各种高清无码截图,还不算附件内容

  10. i18n多语言 解决页面一刷新就恢复默认语言问题