Recently, I am doing something related to the traceless embedding point, and I need to insert the user’s operation and report it. Other events are ok. There are some problems with cell click event, and the initial idea is hook UITableViewCell setSelected(_ selected: Bool, animated: Bool).

But there are two problems with this approach:

  1. It is not easy to get the location of the cell
  2. Even if UITableView’s proxy method doesn’t implement didSelectRowAtIndexPath, it’s going to report a spot

Later, during my discussion with my colleagues, I came up with the idea of using ISa-Swizzling used in KVO to click hookUITableViewCell. This scenario is similar to that of KVO, which is to observe a certain value and call a fixed method when the value changes. And what I need now is to look at the click on the UITableViewCell, and when it clicks, call our method to report the buried point

Here’s how KVO works:

  1. When properties of a class are observed, the system dynamically creates a subclass of that class at run time. And point the isa of the change object to this subclass

  2. If the parent class has setName: or _setName:, then override both methods in the subclass. If both methods exist, only setName: will be overridden (this is the same order as KVCset).

  3. If observed type is nsstrings, then rewrite the method implementation will point _NSSetObjectValueAndNotify this function, if a Bool type, the implementation of rewriting method would point to _NSSetBoolValueAndNotify this function, This function calls willChangeValueForKey: and didChangevlueForKey:, and in between calls, calls the implementation of the parent set method

  4. In willChangeValueForKey: valueForKey: Change [old] in observe didChangevlueForKey: Change [new] in Observe Then call observe’s method – (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary

    *)change context:(nullable void *)context;
    ,>

  5. When use KVC assignment in the NSObject setValue: forKey: method, if the superclass does not exist elegantly-named setName: or the _setName: these methods, _NSSetValueAndNotifyForKeyInIvar will call this function, This function also calls willChangeValueForKey and didChangevlueForKey, if they exist

The click event steps of hook Cell are as follows:

Note: The rule for generating the name of a subclass type is the current class name +” _sub_CZb_tableView_DELEGate_analysis”

  1. Hook UITableView setDelegate method
  2. In setDelegate method determine whether to set the delegate is nil or delegate is not realized the tableView: didSelectRowAtIndexPath: method
  3. If so, set the UITableView’s delegate and end, otherwise proceed to the next step
  4. Determine if the class name of the current class meets the rules for generating the subclass type, if so, set the UITableView’s delegate and end, otherwise proceed to the next step
  5. Check whether the subclass type to be generated is already registered. If not, skip to 7. Otherwise, proceed to the next step
  6. If so, point the Delegate’s ISA to the registered subclass type, and then set the Delegate for the UITableView
  7. Create a subclass of type Delegate and register it
  8. Add a method to this subclass with the same name as the tableView click event proxy and call the parent class’s implementation of this method in this method
  9. Point the Delegate’s ISA to the subclass type you just created

The code is as follows:

typealias TableviewDidSelectRow = @convention(c) (NSObject, Selector, UITableView, IndexPath) -> Void

let czb_didSelectRow:@convention(block) (NSObject, UITableView, IndexPath) -> Void = {
    (this, tableView, indexPath) in
    let superClass: AnyClass? = this.superclass
    let sel = NSSelectorFromString("tableView:didSelectRowAtIndexPath:")
    let method = class_getInstanceMethod(superClass, sel)
    if let impl = class_getMethodImplementation(superClass, sel) {
        let fn = unsafeBitCast(impl, to: TableviewDidSelectRow.self)
        fn(this, sel, tableView, indexPath)
    }
}

extension UITableView {
    static func enableAutoAnalysis () {
       let originalSelector = NSSelectorFromString("setDelegate:")
        let swizzledSelector = #selector(czb_setDelegate(_:))/ / / this method is to hook the corresponding method swizzlingForClass (UITableView. ClassForCoder (), originalSelector: originalSelector, swizzledSelector: swizzledSelector) } @objc func czb_setDelegate(_ delegate: NSObject?) {let sel = NSSelectorFromString("tableView:didSelectRowAtIndexPath:")
        guard let delegate = delegate,delegate.responds(to: sel) else {
            czb_setDelegate(nil)
            return
        }
        var className = NSStringFromClass(delegate.classForCoder)
        if className.hasSuffix("_sub_czb_tableview_delegate_analysis") {
            czb_setDelegate(delegate)
            return
        }
        className += "_sub_czb_tableview_delegate_analysis"
        if let analysisClass = NSClassFromString(className) {
            object_setClass(delegate, analysisClass)
            czb_setDelegate(delegate)
            return
        }
        
        if let customClass = objc_allocateClassPair(delegate.classForCoder, className, 0),
            let method = class_getInstanceMethod(delegate.classForCoder, sel) {
            objc_registerClassPair(customClass)
            let type = method_getTypeEncoding(method)
            let imp = imp_implementationWithBlock(unsafeBitCast(czb_didSelectRow, to: AnyObject.self))
            class_addMethod(customClass, sel, imp, type)
            object_setClass(delegate, customClass)
            czb_setDelegate(delegate)
        }else {
            czb_setDelegate(delegate)
        }
    }
}

Copy the code

Other gains:

  1. @ the use of the convention

    • @convention(swift) : indicates that this is a swift closure
    • @convention(block) : indicates that this is an OC-compatible block closure
    • @convention(c) : closure indicating that this is a c-compatible function pointer
  2. How to convert an IMP to func in Swift and how to create an IMP from a block

    • How to convert IMP to func

      Declare a closure with the same arguments as IMP via typeAlias and @convention(c), for example:

      typealias TableviewDidSelectRow = @convention(c) (NSObject, Selector, UITableView, IndexPath) -> Void

      Using unsafeBitCast function transformation, example: let fn = unsafeBitCast (impl, to: TableviewDidSelectRow. Self)

    • How do I create an IMP from a block

      Create a closure decorated with @convention(block), as in:

      let czb_didSelectRow:@convention(block) (NSObject, UITableView, IndexPath) -> Void = {
          (this, tableView, indexPath) in}}Copy the code

      With imp_implementationWithBlock and unsafeBitCast, for example:

      let block = unsafeBitCast(czb_didSelectRow, to: AnyObject.self)
      let imp = imp_implementationWithBlock(block)
      Copy the code