0

I have a cell with a button inside. In my cellForRowAtIndexPath method i refeer to my uibutton in this way:

UIButton *Button= (UIButton *) [cell viewWithTag:3];

I have found a solution, something like this:

Button.tag = indexPath.row;

[Button addTarget:self action:@selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];

-(void)yourButtonClicked:(UIButton*)sender
{
     if (sender.tag == 0) 
     {
         // Your code here
     }
}

But my button.tag is already used to identify the UIbutton view from the others views inside the cell, i can't use it for the indexpath.row. How i can do in my case?

EDIT

Finally i have made a Category for the UIButton, it seems the faster ad reusable solutions but every answers here is a valid option

user31929
  • 1,115
  • 20
  • 43

3 Answers3

1

You need not access the button using tag and then set Selectorfor each cell. I believe you can achieve much cleaner approach

Step 1:

In your Custom cell, drag the IBAction from button in cell to your custom cell.

@IBAction func buttonTapped(_ sender: Any) {
   //wait for implementation
} 

Step 2:

In your custom cell, now declare a protocol

protocol CellsProtocol : NSObjectProtocol {
    func buttonTapped(at index : IndexPath)
}

and while in being same class, create few variables as well.

weak var delegate : CellsProtocol? = nil
var indexPath : IndexPath! = nil

we will see the usage of these variables soon :)

Step 3:

Lets get back to IBAction we dragged just now

@IBAction func buttonTapped(_ sender: Any) {
     self.delegate?.buttonTapped(at: self.indexPath)
}

Step 4:

Now you can get back to your UITableViewController and confirm to the protocol you declared just now

extension ViewController : CellsProtocol {
    func buttonTapped(at index: IndexPath) {
        //here you have indexpath of cell whose button tapped
    }
}

Step 5:

Now finally update your cellForRowAtIndexPath as

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell : MyTableViewCell = ;
        cell.indexPath = indexPath
        cell.delegate = self
    }
}

Thats it :) Now you have a button action which tells you, button in which cell tapped :) Hope it helps :) This is more generic approach, because even if you have multiple uicomponents you can still use this approach.

EDIT 1:

In comments below rmaddy pointed out that having a indexPath as a property cell might lead to issues and cell should not care to know its indexPath and protocol should be modified to return the cell rather than returning the index path.

Quoting comment :

Not necessarily. You are assuming reloadData is being called. You can have a visible cell and then insert a row above it. That visible cell is not updated and its indexPath is not different but the cell's indexPath property that you have is not updated. Use the cell itself as the argument. It's much better than passing the index path. If the index path is needed by the delegate, the delegate can ask the table view what the cell's current index path is. A cell should never care or know what its index path is.

The statement above makes sense especially the fact that A cell should never care or know what its index path is. Hence updating my answer below

Step 1:

Go ahead and delete the indexPath property in cell :)

Step 2:

Modify protocol to

protocol CellsProtocol : NSObjectProtocol {
    func buttonTapped(in cell : UITableViewCell)
}

Step 3:

Modify your IBAction as

@IBAction func buttonTapped(_ sender: Any) {
    self.delegate?.buttonTapped(in: self)
}

Step 4:

Finally modify your protocol confirmation in your ViewController to

extension ViewController : CellsProtocol {

    func buttonTapped(in cell: UITableViewCell) {
        let indexPath = self.tableView.indexPath(for: cell)
        //here you have indexpath of cell whose button tapped
    }
}

Thats it :)

Sandeep Bhandari
  • 19,999
  • 5
  • 45
  • 78
  • Do not add an `indexPath` property to the cell. That's a bad practice. The cell should pass itself to the delegate, not the index path. The index path of a cell can change as rows are added, removed, or moved in a table view. – rmaddy Nov 15 '17 at 18:47
  • And the question is tagged Objective-C, not Swift. Answers should be posted in the proper language. – rmaddy Nov 15 '17 at 18:49
  • @rmaddy : I know that indexpath of cell will be changed but so will be the property inside cell. Every time cell gets reused cellforRowAtIndexpath will update the new indexpath to cell there by keeping it updated. Passing button as a parameter to delegate and later finding the cell by querying the table view using the center point in button is way too much of hassle and finally sending a button which is primarily a iboutlet in cell kind of makes me feel like a messy approach hence I returned the indexpath which is a mere variable in cell and also avoids all the hassle I mentioned above :) – Sandeep Bhandari Nov 15 '17 at 18:52
  • 1
    Not necessarily. You are assuming `reloadData` is being called. You can have a visible cell and then insert a row above it. That visible cell is not updated and its `indexPath` is not different but the cell's `indexPath` property that you have is not updated. Use the cell itself as the argument. It's much better than passing the index path. If the index path is needed by the delegate, the delegate can ask the table view what the cell's current index path is. A cell should never care or know what its index path is. – rmaddy Nov 15 '17 at 18:55
  • Sorry I didn't realize the question was tagged with objective c the answer I posted is more of proof of concept rather than copy paste able code. So if op asks for objective c I'll surely update the answer :) thank u for finding it out n letting me know :) much appreciated – Sandeep Bhandari Nov 15 '17 at 18:55
  • @rmaddy : sounds interesting and makes lot of sense as well. I'll update my answer mentioning your comment to return cell as a parameter to delegate :) – Sandeep Bhandari Nov 15 '17 at 18:57
  • @rmaddy : Updated my answer to reflect your suggestion :) – Sandeep Bhandari Nov 15 '17 at 19:12
1

Create custom button subclass of UIButton and define property based on your need. Button.tag is a default identifier. In custom class you can give as many property as you want. Then user this Custom button class instead of UIButton. And user custom properties followed by dot(.) as like as tag.

  • There is no need to create a custom button with another property. There are plenty of existing solutions that don't require adding an additional property. – rmaddy Nov 15 '17 at 18:56
  • 1
    @syed-sadrul-ullah-sahad : Could have been a much cleaner solution :) But you are not supposed to subclass UIButton here is why https://stackoverflow.com/questions/13202161/why-shouldnt-i-subclass-a-uibutton – Sandeep Bhandari Nov 15 '17 at 19:15
1

Another option - use a "call back" block.

This assumes you have a Prototype cell with "btnCell" identifier, containing a UIButton connected to the - (IBAction)buttonTapped:(id)sender method shown below...


Your cell class:

//
//  WithButtonTableViewCell.h
//
//  Created by Don Mag on 11/15/17.
//

#import <UIKit/UIKit.h>

@interface WithButtonTableViewCell : UITableViewCell

- (void)setButtonTappedBlock:(void (^)(id sender))buttonTappedBlock;

@end

//
//  WithButtonTableViewCell.m
//
//  Created by Don Mag on 11/15/17.
//

#import "WithButtonTableViewCell.h"

@interface WithButtonTableViewCell ()

@property (copy, nonatomic) void (^buttonTappedBlock)(id sender);

@end

@implementation WithButtonTableViewCell

- (IBAction)buttonTapped:(id)sender {
    // call back if the block has been set
    if (self.buttonTappedBlock) {
        self.buttonTappedBlock(self);
    }
}

@end

Your table view controller class:

//
//  WithButtonTableViewController.h
//
//  Created by Don Mag on 7/12/17.
//

#import <UIKit/UIKit.h>

@interface WithButtonTableViewController : UITableViewController

@end

//
//  WithButtonTableViewController.m
//
//  Created by Don Mag on 11/15/17.
//

#import "WithButtonTableViewController.h"
#import "WithButtonTableViewCell.h"

@interface WithButtonTableViewController ()

@end

@implementation WithButtonTableViewController

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    WithButtonTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"btnCell" forIndexPath:indexPath];

    [cell setButtonTappedBlock:^(id sender) {

        NSLog(@"Button in cell at row %ld in section: %ld was tapped", (long)indexPath.row, (long)indexPath.section);

        // sender is the cell
        // do whatever else you want here...

    }];

    return cell;

}

@end

Now, when the button in the cell is tapped, it will "call back" to the view controller, at which point the code inside the Block will be executed.

DonMag
  • 69,424
  • 5
  • 50
  • 86