@Selector() in Swift

@selector() in Swift?

Swift itself doesn't use selectors — several design patterns that in Objective-C make use of selectors work differently in Swift. (For example, use optional chaining on protocol types or is/as tests instead of respondsToSelector:, and use closures wherever you can instead of performSelector: for better type/memory safety.)

But there are still a number of important ObjC-based APIs that use selectors, including timers and the target/action pattern. Swift provides the Selector type for working with these. (Swift automatically uses this in place of ObjC's SEL type.)

In Swift 2.2 (Xcode 7.3) and later (including Swift 3 / Xcode 8 and Swift 4 / Xcode 9):

You can construct a Selector from a Swift function type using the #selector expression.

let timer = Timer(timeInterval: 1, target: object,
selector: #selector(MyClass.test),
userInfo: nil, repeats: false)
button.addTarget(object, action: #selector(MyClass.buttonTapped),
for: .touchUpInside)
view.perform(#selector(UIView.insertSubview(_:aboveSubview:)),
with: button, with: otherButton)

The great thing about this approach? A function reference is checked by the Swift compiler, so you can use the #selector expression only with class/method pairs that actually exist and are eligible for use as selectors (see "Selector availability" below). You're also free to make your function reference only as specific as you need, as per the Swift 2.2+ rules for function-type naming.

(This is actually an improvement over ObjC's @selector() directive, because the compiler's -Wundeclared-selector check verifies only that the named selector exists. The Swift function reference you pass to #selector checks existence, membership in a class, and type signature.)

There are a couple of extra caveats for the function references you pass to the #selector expression:

  • Multiple functions with the same base name can be differentiated by their parameter labels using the aforementioned syntax for function references (e.g. insertSubview(_:at:) vs insertSubview(_:aboveSubview:)). But if a function has no parameters, the only way to disambiguate it is to use an as cast with the function's type signature (e.g. foo as () -> () vs foo(_:)).
  • There's a special syntax for property getter/setter pairs in Swift 3.0+. For example, given a var foo: Int, you can use #selector(getter: MyClass.foo) or #selector(setter: MyClass.foo).

General notes:

Cases where #selector doesn't work, and naming: Sometimes you don't have a function reference to make a selector with (for example, with methods dynamically registered in the ObjC runtime). In that case, you can construct a Selector from a string: e.g. Selector("dynamicMethod:") — though you lose the compiler's validity checking. When you do that, you need to follow ObjC naming rules, including colons (:) for each parameter.

Selector availability: The method referenced by the selector must be exposed to the ObjC runtime. In Swift 4, every method exposed to ObjC must have its declaration prefaced with the @objc attribute. (In previous versions you got that attribute for free in some cases, but now you have to explicitly declare it.)

Remember that private symbols aren't exposed to the runtime, too — your method needs to have at least internal visibility.

Key paths: These are related to but not quite the same as selectors. There's a special syntax for these in Swift 3, too: e.g. chris.valueForKeyPath(#keyPath(Person.friends.firstName)). See SE-0062 for details. And even more KeyPath stuff in Swift 4, so make sure you're using the right KeyPath-based API instead of selectors if appropriate.

You can read more about selectors under Interacting with Objective-C APIs in Using Swift with Cocoa and Objective-C.

Note: Before Swift 2.2, Selector conformed to StringLiteralConvertible, so you might find old code where bare strings are passed to APIs that take selectors. You'll want to run "Convert to Current Swift Syntax" in Xcode to get those using #selector.

Proper way to use selectors in Swift

  1. why can't I no longer pass simply the function name String to the action?

Using strings for selectors has been deprecated, and you should now write #selector(methodName)instead of "methodName". If the methodName() method doesn't exist, you'll get a compile error – another whole class of bugs eliminated at compile time. This was not possible with strings.


  1. how is the proper way to implement this following the Swift Way? Using the Selector class?

You did it the right way:

button.addTarget(self, action: #selector(ClassName.methodName(_:)), forControlEvents: UIControlEvents.TouchUpInside)


  1. why do we need to pass the @objc keyword and how it affects the function?

In Swift the normal approach is to bind method's calls and method's bodies at compile time (like C and C++ do). Objective C does it at run time. So in Objective C you can do some things that are not possible in Swift - for example it is possible to exchange method's implementation at run time (it is called method swizzling). Cocoa was designed to work with Objective C approach and this is why you have to inform the compiler that your Swift method should be compiled in Objective-C-like style. If your class inherits NSObject it will be compiled ObjC-like style even without @objc keyword.

How to use #selector correctly in Swift?

Currently, my recommendation is using #selector(target.method).

When you pass self as target then, #selector(self.method), passing webView to target, then #selector(webView.method).

#selector is just an encoded ObjC method name (which does not include class name), so both #selector(ClassName.method) and #selector(object.method) should work. But you can expect a better compile-time diagnostic with #selector(target.method).

How to properly fire/call a selector in Swift?

By calling perform(selector) it's like you're calling self.perform(selector) (self is implied), and by doing so the current instance of the ValueAnimator class is the object that actually performs the selector. When that happens, it tries to call a method called temp() of the ValueAnimator class, but as it doesn't exist the app is crashing.

You can verify that if you add a temp() method in the ValueAnimator:

@objc func temp() {
print("Wrong method!!!")
}

If you run now you'll have no crash and the "Wrong Selector!!!" message will appear on the console.

The solution to your problem is to pass the object that should run the selector method along with the selector to the initialisation of the ValueAnimator object.

In the ValueAnimator class declare the following property:

private var target: AnyObject

Update the init method so it can get the target as an argument:

init(durationInSeconds: Int, selector: Selector, target: AnyObject) {
...

self.target = target
}

Also update the timerCallback():

@objc func timerCallback() {
...

_ = target.perform(selector)

...
}

Finally, when you initialise a ValueAnimator instance pass the object that the selector belongs to as well:

let valueAnimatorTest = ValueAnimator(durationInSeconds: 10, selector: #selector(self.temp), target: self)

Run again and the proper temp() method will be executed this time.

I hope it helps.

Have a selector return a value in Swift?

General answer for Have a selector return a value in Swift?

Yes, a selector is just a key for a specific existing method, so if the method returns a value, you may get the returned value invoking the selector.

But, this does not mean you can return a value from an action method.

Specific answer for your case

No, action methods are invoked from inside iOS, and iOS ignores the returned value. (In some cases, calling a method with return value would make your app crash.)

If you need to set cell.button.tag = 5, you may need to write it in your sampleFunction explicitly:

    @objc func sampleFunction(_ gestureRecognizer: MyTapGesture) {
let button = gestureRecognizer.view as! UIButton
button.tag = 5
//...
}

How to pass a selector function as a paramater in Swift?

You can pass your function using a Selector:

class ViewController: UIViewController {
@objc func finished() {
//Do Something here
}
}


class ButtonFactory {
static let shared = ButtonFactory()
func createButton(_ action: Selector) -> UIButton {
let button = UIButton()
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
}


let button = ButtonFactory.shared.createButton(#selector(ViewController.finished))

Note that you should pass the view controller instance as well to your method instead of setting the target to your ButtonFactory shared instance.

Passing Selector properly Swift 5 iOS

Seems you are only passing the selector. You would also need to pass the target which is the view controller and not the view.

init(frame: CGRect, target: Any, editSelector: Selector) {
super.init(frame:frame)
self.target = target
self.editSelector = editSelector
commonInit()
}

var target:Any?
var editSelector:Selector?

And modify initialization as follows:

lazy var titleStackView: TitleStackView = {
let titleStackView = TitleStackView(frame: CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: 44.0)), target: self, editSelector: #selector(self.editSelector(_:)))
titleStackView.translatesAutoresizingMaskIntoConstraints = false
return titleStackView
}()

I have got a better approach than the above, remove the custom init method and use below code:

lazy var titleStackView: TitleStackView = {
let titleStackView = TitleStackView(frame: CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: 44.0)))
titleStackView.button.addTarget(self, action: #selector(self.editSelector(_:)), for: .touchUpInside)
titleStackView.translatesAutoresizingMaskIntoConstraints = false
return titleStackView
}()


Related Topics



Leave a reply



Submit