89

[NOTE This question was originally formulated under Swift 2.2. It has been revised for Swift 4, involving two important language changes: the first method parameter external is no longer automatically suppressed, and a selector must be explicitly exposed to Objective-C.]

Let's say I have these two methods in my class:

@objc func test() {}
@objc func test(_ sender:AnyObject?) {}

Now I want to use Swift 2.2's new #selector syntax to make a selector corresponding to the first of these methods, func test(). How do I do it? When I try this:

let selector = #selector(test) // error

... I get an error, "Ambiguous use of test()." But if I say this:

let selector = #selector(test(_:)) // ok, but...

... the error goes away, but I'm now referring to the wrong method, the one with a parameter. I want to refer to the one without any parameter. How do I do it?

[Note: the example is not artificial. NSObject has both Objective-C copy and copy: instance methods, Swift copy() and copy(sender:AnyObject?); so the problem can easily arise in real life.]

matt
  • 515,959
  • 87
  • 875
  • 1,141

3 Answers3

119

[NOTE This answer was originally formulated under Swift 2.2. It has been revised for Swift 4, involving two important language changes: the first method parameter external is no longer automatically suppressed, and a selector must be explicitly exposed to Objective-C.]

You can work around this problem by casting your function reference to the correct method signature:

let selector = #selector(test as () -> Void)

(However, in my opinion, you should not have to do this. I regard this situation as a bug, revealing that Swift's syntax for referring to functions is inadequate. I filed a bug report, but to no avail.)


Just to summarize the new #selector syntax:

The purpose of this syntax is to prevent the all-too-common runtime crashes (typically "unrecognized selector") that can arise when supplying a selector as a literal string. #selector() takes a function reference, and the compiler will check that the function really exists and will resolve the reference to an Objective-C selector for you. Thus, you can't readily make any mistake.

(EDIT: Okay, yes you can. You can be a complete lunkhead and set the target to an instance that doesn't implement the action message specified by the #selector. The compiler won't stop you and you'll crash just like in the good old days. Sigh...)

A function reference can appear in any of three forms:

  • The bare name of the function. This is sufficient if the function is unambiguous. Thus, for example:

    @objc func test(_ sender:AnyObject?) {}
    func makeSelector() {
        let selector = #selector(test)
    }
    

    There is only one test method, so this #selector refers to it even though it takes a parameter and the #selector doesn't mention the parameter. The resolved Objective-C selector, behind the scenes, will still correctly be "test:" (with the colon, indicating a parameter).

  • The name of the function along with the rest of its signature. For example:

    func test() {}
    func test(_ sender:AnyObject?) {}
    func makeSelector() {
        let selector = #selector(test(_:))
    }
    

    We have two test methods, so we need to differentiate; the notation test(_:) resolves to the second one, the one with a parameter.

  • The name of the function with or without the rest of its signature, plus a cast to show the types of the parameters. Thus:

    @objc func test(_ integer:Int) {}
    @nonobjc func test(_ string:String) {}
    func makeSelector() {
        let selector1 = #selector(test as (Int) -> Void)
        // or:
        let selector2 = #selector(test(_:) as (Int) -> Void)
    }
    

    Here, we have overloaded test(_:). The overloading cannot be exposed to Objective-C, because Objective-C doesn't permit overloading, so only one of them is exposed, and we can form a selector only for the one that is exposed, because selectors are an Objective-C feature. But we must still disambiguate as far as Swift is concerned, and the cast does that.

    (It is this linguistic feature that is used — misused, in my opinion — as the basis of the answer above.)

Also, you might have to help Swift resolve the function reference by telling it what class the function is in:

  • If the class is the same as this one, or up the superclass chain from this one, no further resolution is usually needed (as shown in the examples above); optionally, you can say self, with dot-notation (e.g. #selector(self.test), and in some situations you might have to do so.

  • Otherwise, you use either a reference to an instance for which the method is implemented, with dot-notation, as in this real-life example (self.mp is an MPMusicPlayerController):

    let pause = UIBarButtonItem(barButtonSystemItem: .pause, 
        target: self.mp, action: #selector(self.mp.pause))
    

    ...or you can use the name of the class, with dot-notation:

    class ClassA : NSObject {
        @objc func test() {}
    }
    class ClassB {
        func makeSelector() {
            let selector = #selector(ClassA.test)
        }
    }
    

    (This seems a curious notation, because it looks like you're saying test is a class method rather than an instance method, but it will be correctly resolved to a selector nonetheless, which is all that matters.)

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I don't have Xcode beta installed but wouldn't `#selector(test())` work? – Sulthan Feb 27 '16 at 19:19
  • 2
    Hi @Sulthan, nice to hear from you. - No, that's interpreted a function call. There simply is no way to notate directly the concept "the one with no parameters". It's a hole; they seem to have gone ahead with this without thinking it all the way through (as so often)... – matt Feb 27 '16 at 19:34
  • I just went through the mail discussion "Naming functions with argument labels". The quote is: _Zero-argument function references will still require disambiguation via contextual type information._ Therefore I think you found the correct solution. I don't like the syntax either, I would prefer something like `test(Void)`. Unfortunately, there is a lot of grammar collisions between function call and function descriptor. – Sulthan Feb 27 '16 at 20:57
  • @Sulthan In my bug report I suggested `test(Void)` or even just `test(_)` (as opposed to `test(:_)`. – matt Feb 27 '16 at 21:52
  • 4
    @Sulthan As I feared, the bug report came back "works as intended". So my answer is _the_ answer: you _have_ to use the `as` notation in order to specify the no-parameter variant. – matt Mar 22 '16 at 14:58
  • thanks for the extensive answer. in one part it says `The resolved Objective-C selector, behind the scenes, will still correctly be "test:" (with the colon, indicating a parameter).` so if you're trying to indicate the test with a parameter you have to use `#selector(test(_:))` and cannot use `#selector(test:)`? – Crashalot Apr 03 '16 at 01:50
  • 1
    Another highlight of what an "amazing" experience it is to code in Swift. – Léo Natan Apr 09 '16 at 15:44
  • @LeoNatan Wait until you see what's coming in Swift 3! – matt Apr 09 '16 at 17:08
  • I have, some good, some bad. But these interop edge cases will always remain. :-( – Léo Natan Apr 09 '16 at 17:09
  • hey, if i need to pass the parameter in my #selector(myClass.test:textfield.text) what should I do to entertain this? Please help!! – Mohsin Khubaib Ahmed Apr 28 '16 at 07:31
  • 5
    With the current Swift 3, you have to put the argument list in parentheses: `let selector = #selector(test as (Void) -> Void)`. – Martin R Aug 23 '16 at 07:11
  • Btw, https://bugs.swift.org/browse/SR-1016 (which seems to be about the same problem) is still in the "Open" state. – Martin R Aug 23 '16 at 07:34
  • 1
    Maybe not the best place, but in Swift 3 what will be the preferred syntax? `test as (Void) -> Void` or the shorter syntax `test as () -> ()`? – Dam Dec 06 '16 at 09:42
  • @matt If I do like `let play = #selector(musicPlayerController.play)` I'm still getting `Ambiguous use of play()`... – loretoparisi Jul 12 '17 at 13:47
  • @loretoparisi If you have a specific question please ask it as a question! – matt Jul 13 '17 at 17:26
  • @matt thanks here the specific question https://stackoverflow.com/questions/45099065/ambiguous-use-of-play-of-mpmusicplayercontroller-in-ios11-swift4 – loretoparisi Jul 14 '17 at 09:11
  • 2
    Xcode 9 proposes `#selector(test as () -> Void)`, which works well – Stan Aug 02 '17 at 20:29
  • 2
    @Stan Correct, `Void -> Void` is no longer a legal type. I'll edit to fix; no problem with backward compatibility, as `() -> Void` was always legal. – matt Aug 02 '17 at 21:23
  • Was this broken in Swift 4? I can't get it to work anyhow \: https://pastebin.com/32UwTpsR – hashier Jan 24 '18 at 12:42
  • @hashier The problem is that the syntax for method parameter labels changed (in addition to the need to expose the method explicitly with `@objc` as you have already deduced). I have revised the question and answer to reflect the identical problem and its solution in Swift 4. – matt Jan 24 '18 at 16:36
2

I want to add a missing disambiguation: accessing an instance method from outside the class.

class Foo {
    @objc func test() {}
    @objc func test(_ sender: AnyObject?) {}
}

From the class' perspective the full signature of the test() method is (Foo) -> () -> Void, which you will need to specify in order to get the Selector.

#selector(Foo.test as (Foo) -> () -> Void)
#selector(Foo.test(_:))

Alternatively you can refer to an instance's Selectors as shown in the original answer.

let foo = Foo()
#selector(foo.test as () -> Void)
#selector(foo.test(_:))
Guy Kogus
  • 7,251
  • 1
  • 27
  • 32
  • Yes, the notation `Foo.xxx` is already weird, because these are not outwardly class methods. So it seems the compiler gives you a pass, but only if there is no ambiguity. If there is ambiguity you have to roll back your sleeves and use the longer notation, which is legal and accurate because an instance method is "secretly" a curried class method. Very fine detection of the remaining edge case! – matt Aug 28 '20 at 14:52
0

In my case (Xcode 11.3.1) the error was only when using lldb while debugging. When running it works properly.

Viker
  • 3,183
  • 2
  • 25
  • 25