4

I was wondering, whether it is possible to initialize a UIGestureRecognizer with a block, instead of having to create a separate function for it.
In Swift 3, I believe, this was introduced for timers.

I've implemented something similar to the code posted here since it didn't work for me.
This is my code:

class TapGestureRecognizer: UITapGestureRecognizer {
    private var closure: (() -> ())?

    init() {
        super.init(target: TapGestureRecognizer.self, action: #selector(self.runAction))
    }

    convenience init(for view: UIView, block: @escaping (() -> Void)) {
        self.init()
        closure = block
        view.addGestureRecognizer(self)
    }

    func runAction() {
        print("executed")
        if closure == nil { return }
        closure!()
    }    
}

When I create a TapGestureRecognizer like this:

TapGestureRecognizer(block: { _ in
    print("tapped")
})

... I get the following error:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[MyApp.TapGestureRecognizer runAction]: unrecognized selector sent to class 0x105941598'

Any idea why?

Community
  • 1
  • 1
LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174

1 Answers1

4

Your problem is your target is on on the TapGestureRecognizer class itself. Your runAction method is an instance method, but you're telling the super.init that it's a class method, thus the failure here: +[MyApp.TapGestureRecognizer runAction]. You can fix this by making runAction a class method.

You could also remove the target on class and add the instance after initialization:

init() {
    super.init(target: TapGestureRecognizer.self, action: #selector(runAction))
    self.removeTarget(TapGestureRecognizer.self, action: #selector(runAction))
    self.addTarget(self, action: #selector(runAction))
}

Or you could somehow wrap the closure to be fired in a different object.

mfaani
  • 33,269
  • 19
  • 164
  • 293
JAL
  • 41,701
  • 23
  • 172
  • 300
  • If I automatically add it to the view inside the `convenience init` the compiler gives the warning that the *"Result of initializer is unused"*. (It still works though, obviously.) Any idea how to mute it? I'd rather not manually add it to the view when creating it... Also `let _ = ` wouldn't be the perfect solution. – LinusGeffarth Apr 21 '17 at 06:40
  • @LinusGeffarth I don't get that warning with this code in Xcode 8.3: `view.addGestureRecognizer(TapGestureRecognizer(block: { _ in print("tapped") }))` – JAL Apr 21 '17 at 14:38
  • Right. I was talking about code like this: `TapGestureRecognizer(for: view, block: { _ in print("tapped") })` which automatically adds the recognizer to the view inside its init function. – LinusGeffarth Apr 21 '17 at 14:40
  • @LinusGeffarth That code isn't in your question. Edit your question to include the updated initializer code with `for: view`. – JAL Apr 21 '17 at 14:41
  • @LinusGeffarth All `init` methods return an instance of an object. If you just want to make a convenience method which adds a gesture recognizer to a view, use a void function, not an initializer. – JAL Apr 21 '17 at 14:45
  • As an optimization: doesn't swift figure out itself to convert that instance method to a class method? I guess I'm confusing this optimization mechanism with something else...can't remember what :/ – mfaani Apr 21 '17 at 14:57
  • @Honey I'm not sure what you're asking. If the target is `TapGestureRecognizer.self`, the gesture recognizer is expecting an instance **on that class**. Review the [initializer docs](https://developer.apple.com/reference/uikit/uigesturerecognizer/1624211-init), specifically "`target`: An object that is the recipient of action messages sent by the receiver when it recognizes a gesture. nil is not a valid value." It doesn't matter if a class or instance is used as a target, a message is being sent to whatever it is. – JAL Apr 21 '17 at 15:07
  • I wrote my question bad: My question is: why isn't `runAction` a class method? Because it doesn't have the `class` before it? OK, I get that! But isn't Swift smart enough...to optimize methods that don't use any property into class methods? – mfaani Apr 21 '17 at 15:22
  • 1
    @Honey why would Swift optimize instance methods into class methods? That would be a horrible optimization. The compiler doesn't know how these methods will be called, especially in Objective-C world where any object can send or receive any message, which we are since we're inheriting from `NSObject`. – JAL Apr 21 '17 at 15:41