Steve
Steve

Reputation: 756

UIControl addTarget(_:action:for:) Method in Swift 3.2

I have a class, let's call it ClassA, with the following addTarget call.

awesomeBtn.addTarget(nil, action: #selector(awesomeMethod), for: .touchUpInside)

The compiler accepts the above line when method awesomeMethod is in ClassA (i.e. the same class as the addTarget call).

However, if awesomeMethod is NOT in ClassA, let's say it's in ClassB, then the compiler complains and I am forced to specify the class name in the action.

awesomeBtn.addTarget(nil, action: #selector(ClassB.awesomeMethod), for: .touchUpInside)

In previous versions of Swift (not exactly sure which versions), I could simply write the following, regardless of which class contained the method.

awesomeBtn.addTarget(nil, action:("awesomeMethod"), forControlEvents:.touchUpInside)

Would like to understand why this is or whether I am doing something wrong, thanks.

Upvotes: 1

Views: 1120

Answers (2)

vacawama
vacawama

Reputation: 154523

Yes, they changed it from a String where it was simply a runtime error if you mistyped a method name to a #selector that forces a compile time check for the method. They're just trying to find your errors earlier.

However, if awesomeMethod is NOT in ClassA, let's say it's in ClassB, then the compiler complains and I am forced to specify the class name in the action.

No, you can specify an @objc protocol that implements the method:

@objc protocol AwesomeProtocol {
    func awesomeMethod()
}

Then, even if your class doesn't implement that method, you can specify:

awesomeBtn.addTarget(nil, action: #selector(AwesomeProtocol.awesomeMethod), for: .touchUpInside)

Note: It doesn't seem to be necessary for anyone to adopt that protocol. The button searches up the responder chain and uses the first matching method that it finds. Although, you should adopt the protocol by any class that implements awesomeMethod so that Swift can detect errors in the method signature at compile time.

Upvotes: 1

Sweeper
Sweeper

Reputation: 270790

Basically, Swift is being nice to you :)

In older versions, you use a string literal or the Selector(...) syntax to write a selector. The disadvantage of this is that there is no compile time checking of whether the selector does exist. If you had a typo somewhere you'll only find that out at runtime. You don't need the enclosing class's name because selectors are just the name of a method, not including the enclosing class. At runtime the button's own logic will find that selector in the target object you passed. No one gives a f**k about which class the selector is in. The selector either exists in the target object, or it doesn't.

Now Swift has improved and provided a #selector syntax for writing selectors. This does check whether the selector is valid. The compiler needs to look for a declaration of a method with the same name. This is why you have to add the class name so that the compiler knows where to look for. Otherwise it will just look for it in the current class. However, this does not mean that the selector will always exist at runtime, because at runtime, the button checks whether the target has that selector, so if you pass in a selector of another class (not the class of the target), the button still can't find the selector.

Upvotes: 1

Related Questions