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社区为我们实现了很多功能,我们可在这里查看到社区中的贡献项目。