Search code examples
iosswiftuitapgesturerecognizer

How to add parameters to a generic UITapGestureRecognizer?


The question is: how can I do to make a generic version of UITapGestureRecognizer that I could use across my app.

If I don't want to pass any parameter, it is pretty straightforward:

class ClickListener: UITapGestureRecognizer {
    var onClick : (() -> Void)? = nil
}


// MARK: UIView Extension
extension UIView {
    
    func setOnClickListener(action :@escaping () -> Void){
        let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked(sender:)))
        tapRecogniser.onClick = action
        self.addGestureRecognizer(tapRecogniser)
    }
    
    @objc func onViewClicked(sender: ClickListener) {
        if let onClick = sender.onClick {
            onClick()
        }
    }
  
}

As Sunneet Agrawal did.

The thing is, I sometimes need to pass parameters to this function. For example, if I want to pass an Object, I would like to do so. And then I could reuse this function globally through the app.

Everything I tried didn't work, I can provide a bit of code like this:

class ClickListener<T: Any>: UITapGestureRecognizer {
    var onClick : (() -> Void)? = nil
    var clickedObject: Any
    
    override init(onClick: (() -> Void), clickedObject: Any, target: self, action: ???){
        self.onClick = onClick
        self.clickedObject = clickedObject
        super.init(target: self, action: ???)
    }
}

extension UIView {
    
    func setOnClickListener(action :@escaping () -> Void){
        let tapRecogniser = ClickListener(// Init here)
        tapRecogniser.onClick = action( // parameter here)
        self.addGestureRecognizer(tapRecogniser)
    }
    
    @objc func onViewClicked(sender: ClickListener) {
        if let onClick = sender.onClick {
            onClick(// with parameter passed)
        }
    }
  
}

// Then in my VC

UIView.setOnclickListener(action: anyFunction, clickedObject: anyObject)

But I'm a bit lost right there.


Solution

  • First of all, the selector of the target/action pattern has two fixed forms:

    • A function without parameter
    • A function with one parameter representing the sender, the object which triggered the action.

    But you can add properties in a subclass and pass the parameters in the (custom) init method.

    A generic in a subclass fights the framework therefore a custom dictionary (you could use just Any, too) is the better choice.

    For example beside target and action the class has an userInfo dictionary and a onClick closure. The init method calls super to call the designated initializer of the tap recognizer.

    class ClickListener: UITapGestureRecognizer {
        var onClick : (() -> Void)?
        var userInfo: [String:Any]?
        
        init(target: Any?, action: Selector?, userInfo: [String:Any]? = nil, onClick: (() -> Void)? = nil) {
            self.userInfo = userInfo
            self.onClick = onClick
            super.init(target: target, action: action)
        }
    }
    

    And you can use it

    extension UIView {
        
        func setOnClickListener(action :@escaping () -> Void){
            let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked), userInfo: ["message":"Hello World"], onClick: action)
            self.addGestureRecognizer(tapRecogniser)
        }
        
        @objc func onViewClicked(_ sender: ClickListener) {
            if let userInfo = sender.userInfo,
                let message = userInfo["message"] as? String {
                print(message)
            }
            sender.onClick?()
        }
        
    }
    

    A more generic implementation is to pass the userInfo in the onClick closure.

    class ClickListener: UITapGestureRecognizer {
        var onClick : (([String:Any]?) -> Void)?
        var userInfo: [String:Any]?
        
        init(target: Any?, action: Selector?, userInfo: [String:Any]? = nil, onClick: (([String:Any]?) -> Void)?) {
            self.userInfo = userInfo
            self.onClick = onClick
            super.init(target: target, action: action)
        }
    }
    
    extension UIView {
        
        func setOnClickListener(userInfo: [String:Any], action :@escaping ([String:Any]?) -> Void){
            let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked), userInfo: userInfo, onClick: action)
            self.addGestureRecognizer(tapRecogniser)
        }
        
        @objc func onViewClicked(_ sender: ClickListener) {
            sender.onClick?(sender.userInfo)
        }
        
    }