test() with the #selector(ClassName.test) expression. Ensure the target method is declared in a class and marked with the @objc attribute so the Objective-C runtime can access it.Why this happens
When working with target-action APIs from Cocoa and UIKit—such as Timer, UIBarButtonItem, UIControl.addTarget, or NotificationCenter—the API requires an argument of type Selector (the Swift equivalent of Objective-C’s SEL).
Developers migrating from Objective-C or writing target-action code in Swift often attempt to pass the method directly, such as selector: test(), selector: test, or using legacy Objective-C syntax like @selector(test). Passing test() actually executes the function immediately and returns its return value (typically Void or ()), producing the compiler error Cannot convert value of type '()' to expected argument type 'Selector' (or an overload failure like Could not find an overload for 'init' that accepts the supplied arguments). Passing the uncalled function name test attempts to supply a Swift closure () -> Void, which is also incompatible with Selector.
In addition, Swift does not expose methods to the Objective-C dynamic runtime by default. Even when wrapping a method in #selector(...), the compiler will throw Argument of '#selector' refers to instance method that is not exposed to Objective-C if the method lacks the @objc attribute or belongs to a pure Swift struct or enum.
How to fix it
1. Use the #selector expression and add @objc
Do not call the method or pass it as a closure. Instead, supply the method name inside #selector(...) and prefix the method definition with the @objc modifier.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Correct: Pass the method reference using #selector
let timer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(handleTimerTick),
userInfo: nil,
repeats: true
)
}
// The method must be marked @objc to be visible to the Objective-C runtime
@objc private func handleTimerTick() {
print("Timer triggered")
}
}
The target method can still be private or fileprivate in modern Swift, provided it carries @objc. However, the declaring type must be a class (not a struct or enum) because selectors rely on Objective-C message dispatch.
2. Disambiguate overloaded methods
If your class defines multiple methods with the same base name, referencing #selector(handleTap) will result in a compiler error: Ambiguous use of 'handleTap'. Disambiguate the reference by specifying parameter labels or casting the function signature.
class ActionHandler: NSObject {
func setupButton(button: UIButton) {
// Specify parameter labels to resolve the overload
button.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
}
@objc func handleTap() {
print("Tapped with no sender")
}
@objc func handleTap(_ sender: UIButton) {
print("Tapped by button: \(sender)")
}
}
If one of the overloads has no parameters and another takes parameters with omitted labels (_:), you can resolve the zero-argument overload using an explicit as cast:
let action = #selector((handleTap as () -> Void))
3. Handle property getters and setters
When an Objective-C API expects a selector corresponding to a property accessor, use the getter: or setter: prefix inside the #selector expression:
class ProfileViewModel: NSObject {
@objc dynamic var username: String = ""
}
let getterSelector = #selector(getter: ProfileViewModel.username)
let setterSelector = #selector(setter: ProfileViewModel.username)
This guarantees type safety while preventing hardcoded string typos like "setUsername:".
4. Replace legacy selectors with modern closure-based APIs
Most Apple platform APIs that historically relied on selectors now provide modern, block-based alternatives that eliminate the need for @objc dynamic dispatch and provide stronger type safety.
Modern Timer API
Instead of scheduledTimer(timeInterval:target:selector:userInfo:repeats:), use the block-based initializer available since iOS 10 and macOS 10.12:
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.refreshContent()
}
Remember to capture self weakly to prevent retain cycles.
Modern UIControl Actions (iOS 14+)
Instead of addTarget(_:action:for:), use UIAction closures:
let button = UIButton(type: .system)
let action = UIAction(title: "Submit") { _ in
print("Button pressed via UIAction")
}
button.addAction(action, for: .touchUpInside)
How to prevent it
- Never invoke methods with parentheses in target-action parameters: Writing
selector: myFunc()executes the function rather than passing its identity. - Mark callback functions with
@objc: Every method targeted by#selectormust be visible to the Objective-C runtime. - Prefer closure-based initializers: Use trailing-closure variants of
Timer,NotificationCenter.addObserver, andUIActionwhenever targeting modern deployment targets. - Verify the enclosing type is a reference type: Only
classdeclarations can expose members to Objective-C; structs and actors cannot satisfy target-action requirements directly.
FAQ
Can I pass a selector as a raw String in Swift?
You can initialize a selector using Selector("methodName:"), but doing so disables compile-time verification. If the method name changes or includes a typo, the app will crash at runtime with unrecognized selector sent to instance.
Why can’t I use #selector on a struct method?
Selectors are pointers to Objective-C method names resolved at runtime via dynamic message dispatch. Because Swift struct types are value types that do not participate in Objective-C runtime dispatch, their methods cannot be annotated with @objc or targeted by #selector.
Do I need to make the entire class inherit from NSObject?
Inheriting from NSObject automatically enables Objective-C runtime support for your class. However, in Swift, you only need the individual targeted method to be marked with @objc; inheriting from NSObject is not strictly mandatory for #selector verification unless the API specifically requires an NSObjectProtocol conforming target.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
