Swift
Programming
iOS Development
Selectors
SwiftUI

Passing arguments to selector in Swift

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Selectors are a bridge between Swift and the Objective-C runtime, used heavily in UIKit's target-action pattern. When you add an action to a button or set up a timer, you reference a method by its selector. A common question that trips up developers is how to pass custom arguments through a selector, since the #selector syntax does not support arbitrary parameters directly. Understanding this limitation and the available workarounds is essential for writing clean UIKit code.

How Selectors Work

A selector is essentially a name that identifies a method at runtime. You create one using the #selector expression, which the compiler verifies at build time. The target-action pattern used by UIControl subclasses like UIButton only supports two method signatures for action methods: one with no parameters and one that receives the sender.

swift
1class ViewController: UIViewController {
2    override func viewDidLoad() {
3        super.viewDidLoad()
4
5        let button = UIButton(type: .system)
6        button.setTitle("Tap Me", for: .normal)
7
8        // No parameters
9        button.addTarget(self, action: #selector(handleTapNoArgs), for: .touchUpInside)
10
11        // Sender parameter
12        button.addTarget(self, action: #selector(handleTapWithSender(_:)), for: .touchUpInside)
13    }
14
15    @objc func handleTapNoArgs() {
16        print("Button tapped")
17    }
18
19    @objc func handleTapWithSender(_ sender: UIButton) {
20        print("Tapped button: \(sender.titleLabel?.text ?? "")")
21    }
22}

Notice that both methods are marked @objc. This is required because selectors rely on the Objective-C runtime, and Swift methods are not exposed to that runtime by default.

Why You Cannot Pass Custom Arguments Directly

The #selector expression only captures a method name. It does not capture or forward arguments. The target-action mechanism is designed so that UIKit itself calls your method, passing either nothing or the sender object. There is no built-in slot for additional data.

This means the following will not compile:

swift
// This does NOT work
button.addTarget(self, action: #selector(handleTap(id: 5)), for: .touchUpInside)

Workaround 1: Use the Tag Property

The simplest approach for passing an integer identifier is to use the tag property that every UIView has:

swift
1let button = UIButton(type: .system)
2button.tag = 42
3button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
4
5@objc func buttonTapped(_ sender: UIButton) {
6    let itemId = sender.tag
7    print("Button tapped with tag: \(itemId)")
8}

This works well when you have a list of buttons (for example, in a loop) and each button needs to carry a simple numeric identifier.

Workaround 2: Subclass UIButton to Hold Extra Data

When you need to pass more than a single integer, create a custom button subclass with additional stored properties:

swift
1class DataButton: UIButton {
2    var itemId: String = ""
3    var metadata: [String: Any] = [:]
4}
5
6// Usage
7let button = DataButton(type: .system)
8button.itemId = "user-123"
9button.metadata = ["role": "admin"]
10button.addTarget(self, action: #selector(dataTapped(_:)), for: .touchUpInside)
11
12@objc func dataTapped(_ sender: DataButton) {
13    print("Item: \(sender.itemId), metadata: \(sender.metadata)")
14}

This keeps the data directly on the sender, so the action method has full access without needing external lookups.

Workaround 3: Use a Closure-Based Approach

Starting with iOS 14, UIAction lets you attach a closure directly to a control, bypassing selectors entirely:

swift
1let button = UIButton(type: .system, primaryAction: UIAction { action in
2    let customValue = 42
3    print("Closure called with value: \(customValue)")
4})
5button.setTitle("Tap Me", for: .normal)

Because closures capture their surrounding scope, you can reference any local variable without needing tags or subclasses. This is the most modern and flexible approach.

Workaround 4: Use objc_setAssociatedObject

For cases where you cannot subclass the control, Objective-C associated objects let you attach arbitrary data to any NSObject:

swift
1import ObjectiveC
2
3private var associatedKey = "customData"
4
5extension UIButton {
6    var customData: String? {
7        get { objc_getAssociatedObject(self, &associatedKey) as? String }
8        set { objc_setAssociatedObject(self, &associatedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
9    }
10}
11
12// Usage
13let button = UIButton(type: .system)
14button.customData = "extra-info"
15button.addTarget(self, action: #selector(assocTapped(_:)), for: .touchUpInside)
16
17@objc func assocTapped(_ sender: UIButton) {
18    print("Associated data: \(sender.customData ?? "none")")
19}

Common Pitfalls

  • Forgetting the @objc attribute. Every method referenced by #selector must be marked @objc, or the compiler will produce an error. If your class does not inherit from NSObject, you also need to make it an NSObject subclass.
  • Over-relying on tag for complex data. The tag property is only an Int. Trying to encode multiple values into a single integer leads to fragile code that is hard to maintain.
  • Using selectors in SwiftUI. SwiftUI uses closures and the Combine framework for event handling, not selectors. Attempting to use #selector in a SwiftUI view is a design mismatch.
  • Retaining self in closure-based actions. When using UIAction closures, capturing self strongly can create retain cycles if the button is owned by the same view controller. Use [weak self] when needed.
  • Misspelling the method signature. Before #selector was introduced in Swift 2.2, selectors were string-based and typos caused silent runtime crashes. Always prefer the compiler-checked #selector syntax over Selector("methodName").

Summary

  • Selectors in Swift reference method names at runtime and support either zero parameters or a single sender parameter.
  • You cannot pass custom arguments through #selector directly because the target-action pattern does not support it.
  • Use UIView.tag for simple integer identifiers, a UIButton subclass for richer data, or UIAction closures (iOS 14 and later) to avoid selectors altogether.
  • Always mark selector-referenced methods with @objc and ensure the class inherits from NSObject.
  • In modern UIKit code, prefer closure-based UIAction over selectors for cleaner, more flexible event handling.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.