detecting uibutton pressed in tableview: Swift Best Practices - ios

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

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 {
...
}
}

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
}

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.

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

Swift 3
# cellForRowAt indexPath
cell.Btn.addTarget(self, action: #selector(self.BtnAction(_:)), for: .touchUpInside)
Then
func BtnAction(_ sender: Any)
{
let btn = sender as? UIButton
}

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
}
}
}

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
}

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)")
}

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
}

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
}
}
}

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

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.

Related

tableview issue, indexPath is nil

I have a button and a label in a table view (I am using 8 rows )and for some reason when I click the first button I get indexPath nil error, but when I click the second button (2nd row) I get the first row label. When I click the 3rd row button, I get the second row label etc. Why are they misaligned. I want when I click the first row button to get the first row label etc. Please see the code below. Thank you !!
#objc func btnAction(_ sender: AnyObject) {
var position: CGPoint = sender.convert(.zero, to: self.table)
print (position)
let indexPath = self.table.indexPathForRow(at: position)
print (indexPath?.row)
let cell: UITableViewCell = table.cellForRow(at: indexPath!)! as
UITableViewCell
print (indexPath?.row)
print (currentAnimalArray[(indexPath?.row)!].name)
GlobalVariable.addedExercises.append(currentAnimalArray[(indexPath?.row)!].name)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TableCell else {return UITableViewCell() }
// print(indexPath)
cell.nameLbl.text=currentAnimalArray[indexPath.row].name
// print("\(#function) --- section = \(indexPath.section), row = \(indexPath.row)")
// print (currentAnimalArray[indexPath.row].name)
cell.b.tag = indexPath.row
// print (indexPath.row)
cell.b.addTarget(self, action: #selector(SecondVC.btnAction(_:)), for: .touchUpInside)
return cell
}
Frame math is a worst-case scenario if you have no choice. Here you have a lot of choices.
For example why don't you use the tag you assigned to the button?
#objc func btnAction(_ sender: UIButton) {
GlobalVariable.addedExercises.append(currentAnimalArray[sender.tag].name)
}
A swiftier and more efficient solution is a callback closure:
In TableCell add the button action and a callback property. The outlet is not needed. Disconnect the outlet and connect the button to the action in Interface Builder. When the button is tapped the callback is called.
class TableCell: UITableViewCell {
// #IBOutlet var b : UIButton!
#IBOutlet var nameLbl : UILabel!
var callback : (()->())?
#IBAction func btnAction(_ sender: UIButton) {
callback?()
}
}
Remove the button action in the controller.
In cellForRow assign a closure to the callback property
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// no guard, the code must not crash. If it does you made a design mistake
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableCell
let animal = currentAnimalArray[indexPath.row]
cell.nameLbl.text = animal.name
cell.callback = {
GlobalVariable.addedExercises.append(animal.name)
}
return cell
}
You see the index path is actually not needed at all. The animal object is captured in the closure.
You already pass indexPath.row with button tag. Use the tag as index simply
#objc func btnAction(_ sender: UIButton) {
GlobalVariable.addedExercises.append(currentAnimalArray[sender.tag].name)
}

Access label in a cell in Swift

I have made a custom cell in a table view. There are some buttons and labels in a cell. I'm making a delegate method and call it on the action of the button. The button is also in a cell. Now i'm trying that whenever user press button the label text should increment by one. I'm trying to access the cell label outside the cellForRow delegate method but fail. How can i get the label in a cell outside the cellForRow delegate method in my button action? I have tried some code,
this is in my cell class,
protocol cartDelegate {
func addTapped()
func minusTapped()
}
var delegate : cartDelegate?
#IBAction func addBtnTapped(_ sender: Any) {
delegate?.addTapped()
}
#IBAction func minusBtnTapped(_ sender: Any) {
delegate?.minusTapped()
}
This is in my view controller class,
extension CartViewController : cartDelegate{
func addTapped() {
total += 1
print(total)
}
func minusTapped() {
total -= 1
print(total)
}
}
this is cellForRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CartTableViewCell
cell.dishTitleLbl.text = nameArray[indexPath.row]
cell.priceLbl.text = priceArray[indexPath.row]
price = Int(cell.priceLbl.text!)!
print(price)
cell.dishDetailLbl.text = "MANGO,Apple,Orange"
print(cell.dishDetailLbl.text)
total = Int(cell.totalLbl.text!)!
cell.selectionStyle = .none
cell.backgroundColor = UIColor.clear
cell.delegate = self
return cell
}
I want to access priceLbl in my addTapped and minusTapped functions.
Change your protocol to pass the cell:
protocol cartDelegate {
func addTappedInCell(_ cell: CartTableViewCell)
func minusTappedInCell(_ cell: CartTableViewCell)
}
Change your IBActions to pass the cell:
#IBAction func addBtnTapped(_ sender: Any) {
delegate?.addTappedInCell(self)
}
#IBAction func minusBtnTapped(_ sender: Any) {
delegate?.minusTappedInCell(self)
}
And then your delegate can do whatever it wants to the cell.
To be able to access the label inside of CartViewController but outside of cellForRowAt you have to be able to access a particular cell. To achieve that, since you are dynamically dequeueing reusable cells, you will need an indexPath of that cell and then you can ask the tableView to give you the cell:
// I will here assume it is a third cell in first section of the tableView
let indexPath = IndexPath(row: 2, section: 0)
// ask the tableView to give me that cell
let cell = tableView.cellForRow(at: indexPath) as! CartTableViewCell
// and finally access the `priceLbl`
cell.priceLbl.text = priceArray[indexPath.row]
It should be something as simple as:
self.priceLbl.text = "count = \(total)"

How do you add a button to a tableview so that it extracts user data specific to the cell in which it was pushed?

In my project thus far, I have successfully implemented a tableview whose cells populate postings from FIR database.
I can't figure out what code to add so that when the message user button is pushed, the poster's userID specific to that cell is identified and extracted. If I could get that far, I can extract the rest of the other user's info so that I can set up a chat that includes the receiver of the message's userID, userFirstName, and profile pic.
I would guess that tagging the button is a first step, but I'm not sure how that gets me the userID specific to that posting.
I don't even have code to show because I'm clueless in how to do this....
Put the button in cell. Also make a custom class of cell and put the userId or user object in that cell. When you tap the button you can get the userId etc from that cell.
Or if you want to get that event from cell to viewcontroller you can pass delegate to view viewcontroller. Some thing like this
//cellforrowatindexpath
cell.delegate = self
cell.indexPath = indexPath
// Your cell class
protocol CellDelegate: class {
func didTapCell(index: IndexPath)
}
#IBAction func buttonAction(_ sender: AnyObject) {
self.delegate?.didTapCell(index: indexPath)
}
//tag the cell button
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("HealerProfileCell", forIndexPath: indexPath) as! yourTVCell
cell.detailsButton.tag = indexPath.row
cell.detailsButton.addTarget(self, action: #selector(HealersTableViewController.performHealerDetailsInfoSegue(_:)), forControlEvents: .TouchUpInside)
return cell
}
//make a function
func performHealerDetailsInfoSegue(sender: AnyObject?) {
performSegueWithIdentifier("your segue identifier", sender: sender)
}
//use prepare for segue method
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let object = yourArray[(sender!.tag)]
if segue.identifier == "your segue identifier" {
let dvc = segue.destinationViewController as! YourVC
dvc.object = object
}
}
Add a target to your button in the TableView cellForItemAtIndexPath Method.
cell.yourButton.addTarget(self, action: #selector(CellButtonClicked), for: .touchUpInside)
Then add this function that is called when the button is pushed...
func CellButtonClicked(sender: UIButton) {
print("Clicked")
guard let cellInAction = sender.superview as? YourCell else { return }
guard let din = yourTableView?.indexPath(for: cellInAction) else { return }
// retrieve the specific values in the cell from your array
let specificCellData = yourArray[(indexPath as NSIndexPath).row]
}

Tap button in tableview - wrong cell

Hi I am trying to pass a variable in the cell which is the label when i click on a button in the cell. but the problem is every when I put the code in cellForRowAtIndexPath, it gets the label from the wrong cell every time. why and how to fix this?
var variableToPass: String!
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : MainCell! = tableView.dequeueReusableCellWithIdentifier("MainCell") as! MainCell
variableToPass = label1.text
cell.label1.userInteractionEnabled = true
let tapButton = UITapGestureRecognizer(target: self, action: #selector(ViewController.tapLabel(_:)))
cell.label1.addGestureRecognizer(tapButton)
return cell as MainCell
}
func tapButton(sender:UITapGestureRecognizer) {
performSegueWithIdentifier("SegueIdentifierName", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SegueIdentifierName" {
let viewController = segue.destinationViewController as! ViewController
viewController.getVariablePassed = variableToPass
}
}
The way your code is set up, variableToPass is always the text in the last cell acquired from dequeueReusableCellWithIdentifier. Instead, set variableToPass in your tapButton method, because then you know what cell was selected and you can correctly set variableToPass:
func tapButton(sender:UITapGestureRecognizer) {
let label = sender.view as! UILabel
self.variableToPass = label.text
performSegueWithIdentifier("SegueIdentifierName", sender: self)
}
My way to do this is using a cell delegate and registering my viewController as delegate for the cell, later when the user taps on the UIButton I will call a method like
- (void)buttonWasPressedWithIndexPath:(NSIndexpath*)indexPath
and in cellForRowAtIndexPath I pass the indexPath as a cell property

How to get label on button click in tableview cell for prepareForSegue

I have a tableview. i need to get the label "usernameLabel" from that cell and assign a variable to them. I need to pass that variable to prepareForSegue. The problem is the label is the wrong label from the wrong cell.
here is what i have:
var username: String!
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : MainCell! = tableView.dequeueReusableCellWithIdentifier("MainCell") as! MainCell
username = usernameLabel.text
cell.button.userInteractionEnabled = true
let tapButton = UITapGestureRecognizer(target: self, action: #selector(ViewController.tapLabel(_:)))
cell.button.addGestureRecognizer(tapButton)
return cell as MainCell
}
func tapButton(sender:UITapGestureRecognizer) {
performSegueWithIdentifier("ViewToView2Segue", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ViewToView2Segue" {
let userProfileViewController = segue.destinationViewController as! SecondViewController
secondViewController.usernamePassed = usernamePassed
}
}
simplified question: i need to pass the label.text to another view controller via segue. but currently, it is getting the label from the wrong cell.
cellForRowAtIndexPath method will be multiple times so assigning value in that method will not work, also why are you using tapGesture on button instead of adding action to button, try to change your cellForRowAtIndex like this.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : MainCell! = tableView.dequeueReusableCellWithIdentifier("MainCell") as! MainCell
cell.button.setTitle( usernameLabel.text, forState: .Normal)
cell.button.addTarget(self, action: #selector(self.buttonAction(_:)), forControlEvents: .TouchUpInside)
return cell
}
Now add this buttonAction function inside your ViewController
func buttonAction(sender: UIButton) {
let center = sender.center
let point = sender.superview!.convertPoint(center, toView:self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(point)
let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! MainCell //Add superview on the basis of your button hierarchy in the cell
username = cell.usernameLabel.text
print(username)
performSegueWithIdentifier("ViewToView2Segue", sender: self)
}
You should not save state data in cells. You should have the info that you want in your model (probably an array). When the user taps a cell you should use the indexPath of the selected cell to fetch the info from the model, not from a label on the cell.
(Look up the MVC design pattern for background. A table view cell is a view object, and should not store data. That's the model's job.)

Resources