Home > Net >  Is there a way to get a generic call to any UIButton's method selector?
Is there a way to get a generic call to any UIButton's method selector?

Time:01-27

I've recently come across the technique of swizzling. It looks like it's not a super common practice to use and I can understand the drawbacks I keep seeing listed for it. But as it's a thing that we can use in swift, I was hoping I could throw together something in a playground or practice project.

The idea I had was to use swizzling for analytics purposes. At this point, just to print off what the user is actually doing. I can't really think of interactions the user might make besides tapping buttons on their screen. I figure, I might be able to swizzle UITapGestureRecognizer and make it run a function tracking tap position, etc. But that's too much information for me. I just want to know when a button is tapped. So I think the only thing I need to swizzle is the UIButton. But we set the selector for UIButtons or we set them up as IBActions.

So my question is, Is there a way to get a generic call to any button's method selector? That way, I can swizzle the button in one place. Make it run the analytics and then call the primary function all without actually changing my code in my UIViewController classes directly.

I would post my attempts, but their rather sensical and I couldn't get anything to even run.

Is this a possibility?

CodePudding user response:

You can try swizzling sendAction(_:to:for:) in UIControl. UIControl calls this whenever an event happens for every target-action pair.

extension UIControl {
    // call this at an early point in your app lifecycle
    static func swizzle() {
        if let originalMethod = class_getInstanceMethod(UIControl.self, #selector(sendAction(_:to:for:))),
           let swizzledMethod = class_getInstanceMethod(UIControl.self, #selector(swizzled_sendAction)) {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
    
    @objc func swizzled_sendAction(_ action: Selector,
                                   to target: Any?,
                                   for event: UIEvent?) {
        swizzled_sendAction(action, to: target, for: event)
        if self is UIButton {
            // do some analytics things
        }
    }
}

Caveats:

Since this method is called for every target-action pair, this means that this only works if all your buttons have exactly one target-action pair, and only for the event touchUpInside (a "tap"). In other words, for each button, you only call addTarget once, or add one connection in the storyboard for touchUpInside.

If a button does not have any target-action pairs, then tapping it does nothing. The swizzled method will not be called.

If a button has multiple target-action pairs for touchUpInside, then tapping it would cause the swizzled method to be called multiple times.

If a button has target-action pairs for events other than touchUpInside, then triggering those events would also cause the swizzled method to be called, though this can be prevented to an extent if you carefully check the UIEvent parameter.

To be honest, I would rather just subclass UIButton, instead of swizzling. Make a subclass that does something like this upon its creation:

addTarget(self, action: #selector(doAnalytics), for: .touchUpInside)

...

@objc func doAnalytics() {
    // ...
}
  • Related