Introduction
UIButton.addTarget(_:action:for:) uses the target-action pattern to invoke a method when a button event occurs. The action parameter is a Selector — a reference to an Objective-C method signature — which limits you to specific parameter patterns. You cannot directly pass arbitrary custom parameters through addTarget. This article covers several workarounds to associate custom data with button actions.
Basic addTarget Usage
1let button = UIButton(type: .system)
2button.setTitle("Tap Me", for: .normal)
3
4// No parameters
5button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
6
7@objc func buttonTapped() {
8 print("Button tapped")
9}
10
11// With sender parameter
12button.addTarget(self, action: #selector(buttonTappedWithSender(_:)), for: .touchUpInside)
13
14@objc func buttonTappedWithSender(_ sender: UIButton) {
15 print("Tapped: \(sender.titleLabel?.text ?? "")")
16}
The selector can accept zero parameters, one parameter (sender), or two parameters (sender and event):
@objc func buttonAction(_ sender: UIButton, forEvent event: UIEvent) {
print("Button: \(sender), Event: \(event)")
}
Method 1: Using the tag Property
The simplest way to pass an integer identifier:
1for i in 0..<5 {
2 let button = UIButton(type: .system)
3 button.setTitle("Item \(i)", for: .normal)
4 button.tag = i
5 button.addTarget(self, action: #selector(itemSelected(_:)), for: .touchUpInside)
6 stackView.addArrangedSubview(button)
7}
8
9@objc func itemSelected(_ sender: UIButton) {
10 let index = sender.tag
11 print("Selected item at index: \(index)")
12 let item = items[index]
13 // Process the item
14}
Limitation: tag is only an Int. For complex data, use other methods.
Create a custom button class that holds additional properties:
1class DataButton: UIButton {
2 var itemId: String?
3 var itemData: [String: Any]?
4}
5
6// Usage
7let button = DataButton(type: .system)
8button.itemId = "user-123"
9button.itemData = ["name": "Alice", "role": "admin"]
10button.setTitle("Edit User", for: .normal)
11button.addTarget(self, action: #selector(editUser(_:)), for: .touchUpInside)
12
13@objc func editUser(_ sender: DataButton) {
14 guard let userId = sender.itemId else { return }
15 print("Editing user: \(userId)")
16 if let name = sender.itemData?["name"] as? String {
17 print("Name: \(name)")
18 }
19}
Method 3: Using Associated Objects
Attach arbitrary data to any UIButton without subclassing:
1import ObjectiveC
2
3private var associatedDataKey: UInt8 = 0
4
5extension UIButton {
6 var customData: Any? {
7 get { objc_getAssociatedObject(self, &associatedDataKey) }
8 set { objc_setAssociatedObject(self, &associatedDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
9 }
10}
11
12// Usage
13let button = UIButton(type: .system)
14button.customData = ["id": "order-456", "total": 29.99]
15button.addTarget(self, action: #selector(processOrder(_:)), for: .touchUpInside)
16
17@objc func processOrder(_ sender: UIButton) {
18 guard let data = sender.customData as? [String: Any] else { return }
19 print("Order: \(data["id"] ?? "unknown")")
20}
Method 4: Closure-Based Actions (iOS 14+)
Starting with iOS 14, use UIAction with closures to capture any variables:
1let userId = "user-123"
2let userName = "Alice"
3
4let action = UIAction { [weak self] _ in
5 self?.editUser(id: userId, name: userName)
6}
7
8let button = UIButton(type: .system, primaryAction: action)
9button.setTitle("Edit \(userName)", for: .normal)
10
11func editUser(id: String, name: String) {
12 print("Editing \(name) (id: \(id))")
13}
Or add the action after creation:
1let button = UIButton(type: .system)
2button.setTitle("Delete", for: .normal)
3
4let itemId = "item-789"
5button.addAction(UIAction { [weak self] _ in
6 self?.deleteItem(id: itemId)
7}, for: .touchUpInside)
This is the most modern and clean approach.
Method 5: Using a Dictionary for Mapping
Map buttons to their data through a dictionary:
1class ProductListViewController: UIViewController {
2 private var buttonProductMap: [UIButton: Product] = [:]
3
4 func setupButtons(products: [Product]) {
5 for product in products {
6 let button = UIButton(type: .system)
7 button.setTitle(product.name, for: .normal)
8 button.addTarget(self, action: #selector(productTapped(_:)), for: .touchUpInside)
9 buttonProductMap[button] = product
10 stackView.addArrangedSubview(button)
11 }
12 }
13
14 @objc func productTapped(_ sender: UIButton) {
15 guard let product = buttonProductMap[sender] else { return }
16 print("Selected: \(product.name) — $\(product.price)")
17 }
18}
In UITableView/UICollectionView Cells
For buttons inside cells, use closures or delegate patterns:
1class ProductCell: UITableViewCell {
2 var onButtonTapped: ((Product) -> Void)?
3 private var product: Product?
4
5 func configure(with product: Product) {
6 self.product = product
7 buyButton.addAction(UIAction { [weak self] _ in
8 guard let product = self?.product else { return }
9 self?.onButtonTapped?(product)
10 }, for: .touchUpInside)
11 }
12}
13
14// In the view controller
15func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
16 let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell") as! ProductCell
17 let product = products[indexPath.row]
18 cell.configure(with: product)
19 cell.onButtonTapped = { [weak self] product in
20 self?.addToCart(product)
21 }
22 return cell
23}
Common Pitfalls
Retain cycles: When using closures (UIAction) that capture self, always use [weak self] to avoid retain cycles that prevent the view controller from being deallocated.
Cell reuse with tag: In UITableView, cells are reused. If you use button.tag = indexPath.row, the tag becomes stale after insertions/deletions. Prefer closure-based approaches for cells.
Selector string typos: Using #selector provides compile-time checking. Never use Selector("methodName") strings — they crash at runtime if misspelled.
@objc requirement: Methods referenced by #selector must be marked @objc, which requires the class to inherit from NSObject (or be marked @objcMembers).
Multiple actions: addTarget can attach multiple actions to the same button. Calling it twice with different selectors registers both — they all fire on tap. Use removeTarget first if replacing an action.
Summary
addTarget does not support arbitrary parameters — only the sender and event
Use button.tag for simple integer identifiers
Subclass UIButton or use associated objects for complex data
Use UIAction closures (iOS 14+) for the cleanest approach with captured variables
In table/collection view cells, use closure callbacks to avoid stale tag values