43

I have a tableview with a variable number of cells representing students that correspond to their particular instructor. They are custom cells with a button that triggers a segue to a new VC, bringing up detailed information on the student whose cell it was. My question is:

What is the best practice in swift for identifying which button was pressed?

Once i know the index path, I can identify which student's information needs to be passed to the next VC. There is a great answer for objective C in the post below, but I'm not sure how to translate to Swift. Any help would be much appreciated.

Detecting which UIButton was pressed in a UITableView

Community
  • 1
  • 1
jamike
  • 443
  • 1
  • 4
  • 6

12 Answers12

91

If your code allows, I'd recommend you set the UIButton tag equal to the indexPath.row, so when its action is triggered, you can pull the tag and thus row out of the button data during the triggered method. For example, in cellForRowAtIndexPath you can set the tag:

button.tag = indexPath.row
button.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)

then in buttonClicked:, you can fetch the tag and thus the row:

func buttonClicked(sender:UIButton) {

    let buttonRow = sender.tag
}

Otherwise, if that isn't conducive to your code for some reason, the Swift translation of this Objective-C answer you linked to:

- (void)checkButtonTapped:(id)sender
{
    CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
    if (indexPath != nil)
    {
     ...
    }
}

is:

func checkButtonTapped(sender:AnyObject) {
      let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
    let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
    if indexPath != nil {
        ...
    }
}
Community
  • 1
  • 1
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • Just implemented it and the top alternative worked very well, thank you. – jamike Dec 11 '14 at 22:37
  • In swift 3 the second solution's 2nd & 3rd line become : let buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: self.tableView);;;;; let indexPath = self.tableView.indexPathForRow(at: buttonPosition) – Kiran P Nair Mar 04 '17 at 12:51
  • 4
    Do not use the tag approach. It has many issues when rows can be inserted, deleted, or moved. Always use the second solution. – rmaddy May 23 '17 at 15:27
  • change your action to #selector(buttonClicked(sender:)) – Umit Kaya Oct 07 '17 at 17:38
15

Swift 3.0 Solution

cell.btnRequest.tag = indexPath.row

    cell.btnRequest.addTarget(self,action:#selector(buttonClicked(sender:)), for: .touchUpInside)

    func buttonClicked(sender:UIButton) {

            let buttonRow = sender.tag
        }
Sourabh Sharma
  • 8,222
  • 5
  • 68
  • 78
  • 3
    This one worked for me!! The others weren't working due to how the selector was being set up in them I think. Thank you! – Ben987654 Jan 28 '17 at 23:11
  • 2
    Do not use this approach. Using tags to track a row fails under many conditions. – rmaddy May 23 '17 at 15:42
6

Updated for Swift 3

If the only thing you want to do is trigger a segue on a touch, it would be against best practice to do so via a UIButton. You can simply use UIKit's built in handler for selecting a cell, i.e. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath). You could implement it doing something like the following:

Create a custom UITableViewCell

class StudentCell: UITableViewCell {
    // Declare properties you need for a student in a custom cell.
    var student: SuperSpecialStudentObject!

    // Other code here...
}

When you load your UITableView, pass the data into the cell from you data model:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "StudentCell", for: indexPath) as! StudentCell
    cell.student = superSpecialDataSource[indexPath.row]
    return cell
}

Then use didSelectRow atIndexPath to detect when a cell has been selected, access the cell and it's data, and pass the value in as a parameter to performSegue.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath) as! StudentCell

    if let dataToSend = cell.student {
        performSegue(withIdentifier: "DestinationView", sender: dataToSend)
    }
}

And finally in prepareForSegue:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "DestinationView" {
        let destination = segue.destination as! DestinationViewController
        if let dataToSend = sender as? SuperSpecialStudentObject {
            destination.student = dataToSend
        }
    }
}

Alternatively if you want them to only select a part of the cell instead of when they touch anywhere inside the cell, you could add an accessory item onto your cell such as the detail accessory item (looks like the circle with an "i" inside of it) and use override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) instead.

darksinge
  • 1,828
  • 1
  • 21
  • 32
  • I have seen you tried to update another answer to Swift 2.0 this is appreciated, but you need to post as own answer (it will be rejected)... (I have commented on edit as well, but for you to see I comment also here), keep up the good work – Petter Friberg Jan 28 '16 at 22:04
2

Another possible solution would be using dispatch_block_t. If you do it with Storyboard you first have to create a member variable in your custom UITableViewCell class.

var tapBlock: dispatch_block_t?

Then you have to create an IBAction and call the tapBlock.

@IBAction func didTouchButton(sender: AnyObject) {
    if let tapBlock = self.tapBlock {
        tapBlock()
    }
}

In your view controller with the UITableView you can simply react to the button events like this

let cell = tableView.dequeueReusableCellWithIdentifier("YourCellIdentifier", forIndexPath: indexPath) as! YourCustomTableViewCell

cell.tapBlock = {
   println("Button tapped")
}

However you have to be aware when accessing self inside the block, to not create a retain cycle. Be sure to access it as [weak self].

gpichler
  • 2,181
  • 2
  • 27
  • 52
2

Swift 3

@ cellForRowAt indexPath

cell.Btn.addTarget(self, action: #selector(self.BtnAction(_:)), for: .touchUpInside)

Then

func BtnAction(_ sender: Any)
    {

        let btn = sender as? UIButton

    }
legoscia
  • 39,593
  • 22
  • 116
  • 167
2

It's never a good idea to use tags to identify cells and indexPaths, eventually you'll end up with a wrong indexPath and consequently the wrong cell and information.

I suggest you try the code bellow (Working with UICollectionView, didn't tested it with a TableView, but it probably will work just fine):

SWIFT 4

@objc func buttonClicked(_ sender: UIButton) {
    if let tableView = tableViewNameObj {
        let point = tableView.convert(sender.center, from: sender.superview!)

        if let wantedIndexPath = tableView.indexPathForItem(at: point) {
            let cell = tableView.cellForItem(at: wantedIndexPath) as! SpecificTableViewCell

        }
    }
}
2

Detecting the Section and row for UiTableView indexPath on click Button click

//MARK:- Buttom Action Method
    @objc func checkUncheckList(_sender:UIButton)
    {
        if self.arrayRequestList != nil
        {

          let strSection = sender.title(for: .disabled)

          let dict = self.arrayRequestList![Int(strSection!)!]["record"][sender.tag]

          print("dict:\(dict)")

          self.requestAcceptORReject(dict: dict, strAcceptorReject: "1")
        }

    }

Here is UITableView Cell Method to add the targate

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "OtherPropertySelectiingCell", for: indexPath as IndexPath) as! OtherPropertySelectiingCell
        cell.btnAccept.tag = indexPath.row
        cell.btnAccept.setTitle("\(indexPath.section)", for: .disabled)
        cell.btnAccept.addTarget(self, action: #selector(checkUncheckList(_sender:)), for: .touchUpInside)
        return cell
    }
pansora abhay
  • 892
  • 10
  • 16
2

Swift 5. In cellForRowAtIndexPath you set the tag:

cell.shareButton.tag = indexPath.row
cell.shareButton.addTarget(self, action: #selector(shareBtnPressed(_:)), for: .touchUpInside)

Then in shareBtnPressed you fetch the tag

  @IBAction func shareBtnPressed(_ sender: UIButton) {

    let buttonRow = sender.tag

    print("Video Shared in row \(buttonRow)")
}
Timchang Wuyep
  • 629
  • 7
  • 10
1

As a follow up to @Lyndsey and @longbow's comments, I noticed that when I had the segue in storyboard going from the button to the destinationVC, the prepareForSegue was being called before the buttonClicked function could update the urlPath variable. To resolve this, I set the segue directly from the first VC to the destinationVC, and had the segue performed programmatically after the code in buttonClicked was executed. Maybe not ideal, but seems to be working.

func buttonClicked(sender:UIButton) {
    let studentDic = tableData[sender.tag] as NSDictionary
    let studentIDforTherapyInt = studentDic["ID"] as Int
    studentIDforTherapy = String(studentIDforTherapyInt)
    urlPath = "BaseURL..."+studentIDforTherapy
    self.performSegueWithIdentifier("selectTherapySegue", sender: sender)
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
    if (segue.identifier == "selectTherapySegue") {
        let svc = segue.destinationViewController as SelectTherapyViewController;
        svc.urlPath = urlPath
    }
jamike
  • 443
  • 1
  • 4
  • 6
1

Updated for Swift 5:

Place the following code within your ViewController class

@IBAction func buttonClicked(_ sender: UIButton) {
    if let tableView = tableView {
        let point = tableView.convert(sender.center, from: sender.superview!)

//can call wantedIndexPath.row here

        }
    }
}
helloworld12345
  • 176
  • 1
  • 4
  • 22
0

I am doing it via prepareforSegue

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

            let indexPath = self.tableView.indexPathForSelectedRow()
            let item = tableViewCollection[indexPath!.row].id
            let controller = segue.destinationViewController as? DetailVC

            controller?.thisItem = item
    }

and on the next controller i will just reload the full item properties, by knowing its id and setting it to the var thisItem in the DetailVC

longbow
  • 1,593
  • 1
  • 16
  • 39
  • 1
    This won't work for the OP's problem, because the row isn't selected when the button is pressed. – rdelmar Dec 11 '14 at 18:59
  • actually i got the same problem, the only thing that you have to add is a manual segue to your button, and give out tags to your buttons corresponding with your indexpathrow - its stupid but it works //code follows – longbow Dec 11 '14 at 19:09
0

I was going to use the indexPath approach until I came to understand that it would be unreliable/wrong in some situations (deleted or moved cell, for instance).

What I did is simpler. By example, I am displaying a series of colors and their RGB values—one per tableview cell. Each color is defined in an array of color structures. For clarity these are:

struct ColorStruct {
    var colorname:String    = ""
    var red:   Int = 0
    var green: Int = 0
    var blue:  Int = 0
}

var colors:[ColorStruct] = []       // The color array

My prototype cell has a var to hold the actual index/key into my array:

class allListsCell: UITableViewCell {
    @IBOutlet var cellColorView: UIView!
    @IBOutlet var cellColorname: UILabel!
    var colorIndex = Int()  // ---> points directly back to colors[]

    @IBAction func colorEditButton(_ sender: UIButton, forEvent event: UIEvent) {
        print("colorEditButton: colors[] index:\(self.colorIndex), \(colors[self.colorIndex].colorname)")
    }
}

This solution takes three lines of code, one in the prototype cell definition, the second in the logic that populates a new cell, and the the third in the IBAction function which is called when any cell's button is pressed. Because I have effectively hidden the "key" (index) to the data in each cell AS I am populating that new cell, there is no calculation required -and- if you move cells there is no need to update anything.

Vic W.
  • 817
  • 1
  • 7
  • 9