Swift
iOS Development
Button Actions
addTarget
Programming Tips

Attach parameter to button.addTarget action in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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

swift
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):

swift
@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:

swift
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.

Method 2: Subclassing UIButton

Create a custom button class that holds additional properties:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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

Course illustration
Course illustration

All Rights Reserved.