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

 

posted @ 2022-02-23 16:45  zbblogs  阅读(717)  评论(0编辑  收藏  举报