This question already has answers here:
iOS get specific UITableViewCell
(5 answers)
Correct way to setting a tag to all cells in TableView
(3 answers)
Closed 5 years ago.
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
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.
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 :)
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.
I've created custom tableView Controller, inside the cell i've placed a button to open the device photo library. My problem, i cant able to open imagePickerController from CustomCell.m, its shows below error.
Please give some idea to fix my issue.
TableViewCell is a view, you can not present on views instead UIViewController can handle it. You should transfer the control from your cell to your controller that holds tableview and creates custom cell for it.
Try like this:
Custom Cell .h Class:
#protocol changePictureProtocol <NSObject>
-(void)loadNewScreen:(UIViewController *)controller;
#end
#property (nonatomic, retain) id<changePictureProtocol> delegate;
Then Synthesize it in.m.
Add this in m file:
-(IBAction)changePicture:(id)sender
{
// ..... blah blah
[self.delegate loadNewScreen:picker];
}
The viewcontroller that loads this cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// create cell here
cell.delegate = self;
}
-(void)loadNewScreen:(UIViewController *)controller;
{
[self presentViewController:controller animated:YES completion:nil];
}
Its a psuedocode to give you an idea.
EDIT:
Swift equivalent:
CustomTableViewCell.swift code:
protocol ChangePictureProtocol : NSObjectProtocol {
func loadNewScreen(controller: UIViewController) -> Void;
}
class CustomTableViewCell: UITableViewCell {
// Rest of the class stuff
weak var delegate: ChangePictureProtocol?
#IBAction func changePicture(sender: AnyObject)->Void
{
var pickerVC = UIImagePickerController();
if((delegate?.respondsToSelector("loadNewScreen:")) != nil)
{
delegate?.loadNewScreen(pickerVC);
}
}
}
ViewController.swift code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier") as CustomTableViewCell!
cell.delegate = self;
return cell;
}
func loadNewScreen(controller: UIViewController) {
self.presentViewController(controller, animated: true) { () -> Void in
};
}
The answers proposing a delegate or instance variable are correct, but, however, in most cases it is not important to use a special view controller to present the new controller.
In these cases the following solution is much simpler: Just use the applications root view controller:
UIViewController* activeVC = [UIApplication sharedApplication].keyWindow.rootViewController;
[activeVC presentViewController:'new view controller'
animated:YES
completion:NULL];
presentViewController: message is there for Viewcontroller. Do delegate the control from Cell to viewController and use the same line would solve your problem. UITableViewCell does not responds to presentViewController: message.
You need a UIViewController to present from. There are a few things you can do here. One would be to create a custom delegate protocol with a changePicturePressed callback method. Then you can assign the containing view controller as that delegate and perform the presentation in the delegate method.
The other thing you can do is pass the UIViewController into your cell during initialization and set it as a weak property. Then you can perform the presentation directly from inside the cell using that property.
Make IBOutlet of your button
Then in cellForRowAtIndexPath give target to the button
CustomCell *customCell=[tableView dequeueReusableCellWithIdentifier:#"CellIdentifier"];
[customCell.yourButton addTarget:self action:#selector(yourButtonAction:) forControlEvents:UIControlEventTouchUpInside];
One other way to do this is pass reference of the controller of the CustomCell to the CustomCell and use that to present the new controller.
Swift:
CustomCell.swift :
class CustomTableViewCell: UITableViewCell {
// Rest of the class stuff
var parentViewController: UIViewController? = nil
gotoNewControllerBtn.addTarget(self, action: #selector(gotoNewController), for: .touchUpInside)
func gotoNewController() {
let newViewController = NewViewController()
parentViewController.present(newViewController, animated: true, completion: nil)
}
}
ViewController.swift:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier") as CustomTableViewCell!
cell.parentViewController = self;
return cell;
}
You need viewcontroller for presenting the view. You cannot present viewcontroller on tableviewcells.
I am trying to implement 3 tables in a single segue in a storyboard.
When one table is selected it will unhidden a view with another table and likewise one more.
The following code i have used for one table the cell format of each table is different and rows also vary. So how can i DIFFERENTIATE between each table by coding to set different number of rows for each table and so on?
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 3;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell2";
UITableViewCell *cell1 = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell1==nil)
{
cell1=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
temp=[array objectAtIndex:indexPath.row];
UILabel *Label1 = (UILabel *)[cell1 viewWithTag:4];
Label1.text = temp.Title;
UILabel *Label2 = (UILabel *)[cell1 viewWithTag:6];
Label2.text = temp.Title;
UITextField *textfield1 = (UITextField *)[cell1 viewWithTag:5];
textfield1.text =temp.description;
UILabel *Label3 = (UILabel *)[cell1 viewWithTag:7];
Label3.text = temp.Title;
return cell1;
}
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.showlist=[[ShowList alloc]initWithNibName:#"ShowList" bundle:nil];
[tableView deselectRowAtIndexPath:indexPath animated:NO];
ShowlistIndex=indexPath.row;
_secondview.hidden=NO;
}
You should declare your tableViews in .h file.
#property (weak, nonatomic) UITableView *firstTableView;
#property (weak, nonatomic) UITableView *secondTableView;
#property (weak, nonatomic) UITableView *thirdTableView;
And then all the delegate methods have variable with pointing witch object call this method, so you can check:
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(tableView == self.firstTableView)
return 3;
else if(tableView == self.secondTableView)
return 4;
else if(tableView == self.thirdTableView)
return 100;
}
The other delegate methods work in the same way.
You can keep track of the different tableviews using class properties e.g.:
#property (nonatomic, strong) UITableView *tableView1;
#property (nonatomic, strong) UITableView *tableView2;
#property (nonatomic, strong) UITableView *tableView3;
In the delegate methods you can check for the correct tableView e.g.:
if (tableView == self.tableView1) {
// add code for tableView1
} else if (tableView == self.tableView2) {
// add code for tableView2
} else if (tableView == self.tableView3) {
// add code for tableView3
} else {
// unknown tableView
}
You have the tableview reference in each of your delegate methods right? You can find out which tableview you are currently walking through based on that..
Assuming..
IBOutlet UITableView *tableView1;
IBOutlet UITableView *tableView1;
IBOutlet UITableView *tableView1;
Ex:
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
if(tableView == tableView1)
return 1;
if(tableView == tableView2)
return 5;
return 10;
}
You can do the same for the other delegate methods..
I hope I understood your question correctly..
you nedd to create 3 uitableview outlets. and then you can identifies tableviews by specifyin tag. for example tableview1.tag=1, tableview.tag=2 etc.
then in tableview methods you can use it. for example.
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(tableView.tag==1){
return 3;
}
else if(tableView=2)
{return 3;}
}
may be this help.
Swift 4.1
4.1 is a version which i wrote the code. It might be worked in the other version of swift. I'm not sure.
Firstly , right click to the tableViewContent1 and tableViewContent2 and drag it to dataSource and
delegate into View Controller like this image below.
Secondly , you can implement as my code below.
* I've already set the both label in tableViewContent1 and tableViewContent2 with tag = 1000 .
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableViewContent1: UITableView!
#IBOutlet weak var tableViewContent2: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController :
UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == tableViewContent1 {
// in order to make the tableViewContent1 get 5 rows.
return 5
}
else{
// in order to make the tableViewContent2 get 3 rows.
return 3
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == self.tableViewContent1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "content1", for: indexPath) as! TableViewCellFirstCustom
let label : UILabel = cell.viewWithTag(1000) as! UILabel{
lab.text = "to change label in content1."
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "content2", for: indexPath)
let label : UILabel = cell.viewWithTag(1000) as! UILabel
label.text = "to change label in content2."
return cell
}
}
}
after that you will get result like image below.
It worked for me.
i want to animate the UICollectionViewCell when action is called.
i have done UICollectionViewCell in Interface Builder, the UICollectionView also.
Now i want to get the correct indexPath at my actionBtnAddToCard method.
thats the way i try it now (method in ProduktViewCell.m):
- (IBAction)actionAddToCart:(id)sender {
XLog(#"");
// see this line
NSIndexPath *indexPath = ??** how can i access the correct indexPath**??;
SortimentViewController *svc = [[SortimentViewController alloc] initWithNibName:#"SortimentViewController_iPad" bundle:[NSBundle mainBundle]];
[svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
[svc collectionView:svc.collectionViewProdukte didSelectItemAtIndexPath:indexPath];
}
SortimentViewController is the viewController which inherits the UICollectionView.
how to acces the correct indexPath?
UPDATE 1: edited post for better understanding.
- (IBAction)actionAddToCart:(id)sender {
NSIndexPath *indexPath;
indexPath = [self.collectionView indexPathForItemAtPoint:[self.collectionView convertPoint:sender.center fromView:sender.superview]];
...
}
if you know the view hierarchy it is easy.
UIButton *button = (UiButton *) sender;
if the button is like this - > UITableViewCell - > button
then you can get cell like this
UITableViewCell *cell = (UITableViewCell *)[button superview];
if the button is like this - > UITableViewCell - > content view -> button
UITableViewCell *cell = (UITableViewCell *)[[button superview] superview];
and finally index path can be extracted like this
NSIndexPath *indexPath = [self.table_View indexPathForCell:cell];
Do Not Depend on view.
Try this.
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:buttonPosition];
NSLog(#"%ld", (long)indexPath.row);
Using code like [[button superview] superview] is fragile and not future-proof; indeed, it's not even guaranteed to work on all iOS versions unless you explicitly test it. I always use an iterative helper method for this purpose:-
- (UIView *)superviewWithClassName:(NSString *)className fromView:(UIView *)view
{
while (view)
{
if ([NSStringFromClass([view class]) isEqualToString:className])
{
return view;
}
view = view.superview;
}
return nil;
}
Then I call it from the button handler like so:-
- (IBAction)buttonClicked:(id)sender
{
UIButton *button = (UIButton *)sender;
UICollectionViewCell *cell = (UICollectionViewCell *)
[self superviewWithClassName:#"UICollectionViewCell"
fromView:button];
if (cell)
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
// do whatever action you need with the indexPath...
}
}
UPDATE: Swift version of superviewWithClassName. Made it a class method since it never references self.
static func superviewWithClassName(className:String, fromView view:UIView?) -> UIView? {
guard let classType = NSClassFromString(className) else {
return nil
}
var v:UIView? = view
while (v != nil) {
if v!.isKindOfClass(classType) {
return v
}
v = v!.superview
}
return nil
}
and some code to call it, either from prepareForSegue or a button handler:-
guard let cell = UIView.superviewWithClassName("UICollectionViewCell", fromView: sender as? UIView) as? UITableViewCell else {return}
Swift solution:
A UICollectionView extension like this one can be useful for this.
extension UICollectionView {
func indexPathForView(view: AnyObject) -> NSIndexPath? {
let originInCollectioView = self.convertPoint(CGPointZero, fromView: (view as! UIView))
return self.indexPathForItemAtPoint(originInCollectioView)
}
}
Usage becomes easy everywhere.
let indexPath = collectionView.indexPathForView(button)
You can do it like this, indexPathsForVisibleItems will return array of NSIndexPaths for items currently visible on view and first object returns the first one (if you have one cell per view).
NSIndexPath *indexPath = [[svc.collectionViewProdukte indexPathsForVisibleItems] firstObject]
If you want to animate a specific cell, you need to get a reference to that cell. Simply calling
[svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
does nothing. You need to keep the cell that the method returns, like this:
UICollectionViewCell *cell = [svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
After that, go ahead and animate:
[UIView animateWithDuration:0.2f animations:^{
cell.transform = CGAffineTransformMakeScale(0.5f, 0.5f);
}];
Swift 3 Solution : Based on Ishan Handa's Answer
extension UICollectionView {
func indexPathForView(view: AnyObject) -> IndexPath? {
let originInCollectioView = self.convert(CGPoint.zero, from: (view as! UIView))
return self.indexPathForItem(at: originInCollectioView) as IndexPath?
}
}
Usage:
func deleteCell(sender:UIButton){
var indexPath:IndexPath? = nil
indexPath = self.collectionView.indexPathForView(view: sender)
print("index path : \(indexPath)")
}
//Note: this is for a storyboard implementation
// here is code for finding the row and section of a textfield being edited in a uicollectionview
UIView *contentView = (UIView *)[textField superview];
UICollectionViewCell *cell = (UICollectionViewCell *)[contentView superview];
cell = (UICollectionViewCell *)[contentView superview];
// determine indexpath for a specific cell in a uicollectionview
NSIndexPath *editPath = [myCollectionView indexPathForCell:cell];
int rowIndex = editPath.row;
int secIndex = editPath.section;
Even though many answer i found here .this will be shortest and useful irrespective of the view hierarchy
- (void) actionAddToCart:(id)sender
{
id view = [sender superview];
while (view && [view isKindOfClass:[UICollectionViewCell class]] == NO)
{
view = [view superview];
}
NSIndexPath *thisIndexPath = [self.collectionView indexPathForCell:view];
NSLog(#"%d actionAddToCart pressed",thisIndexPath.row);
}
Xcode10. Swift 4.2 version.
extension UICollectionView {
func indexPathForView(view: AnyObject) -> IndexPath? {
guard let view = view as? UIView else { return nil }
let senderIndexPath = self.convert(CGPoint.zero, from: view)
return self.indexPathForItem(at: senderIndexPath)
}
}
Usage:
// yourView can be button for example
let indexPath = collectionView.indexPathForView(view: yourView)
You almost certainly have a UICollectionViewCell subclass. Just add a property and set the indexPath in cellForItemAtIndexPath.
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: “cell_id”, for: indexPath)
let bttn_obj = UIButton(frame: CGRect(x: 5.5, y: 5.5, width: 22, height: 22))
bttn_obj.addTarget(self, action: #selector(bttn_action), for: UIControl.Event.touchUpInside)
cell.addSubview(bttn_obj)
return cell
}
#IBAction func bttn_action(_ sender: UIButton) -> Void {
let cell_view = sender.superview as! UICollectionViewCell
let index_p : IndexPath = self.collectionview.indexPath(for: cell_view)!
print(index_p)
}
I have a UITableView with 5 UITableViewCells. Each cell contains a UIButton which is set up as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = #"identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
[cell autorelelase];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
[button addTarget:self action:#selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
[button setTag:1];
[cell.contentView addSubview:button];
[button release];
}
UIButton *button = (UIButton *)[cell viewWithTag:1];
[button setTitle:#"Edit" forState:UIControlStateNormal];
return cell;
}
My question is this: in the buttonPressedAction: method, how do I know which button has been pressed. I've considered using tags but I'm not sure this is the best route. I'd like to be able to somehow tag the indexPath onto the control.
- (void)buttonPressedAction:(id)sender
{
UIButton *button = (UIButton *)sender;
// how do I know which button sent this message?
// processing button press for this row requires an indexPath.
}
What's the standard way of doing this?
Edit:
I've kinda solved it by doing the following. I would still like to have an opinion whether this is the standard way of doing it or is there a better way?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = #"identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
[cell autorelelase];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
[button addTarget:self action:#selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:button];
[button release];
}
UIButton *button = (UIButton *)[cell.contentView.subviews objectAtIndex:0];
[button setTag:indexPath.row];
[button setTitle:#"Edit" forState:UIControlStateNormal];
return cell;
}
- (void)buttonPressedAction:(id)sender
{
UIButton *button = (UIButton *)sender;
int row = button.tag;
}
What's important to note is that I can't set the tag in the creation of the cell since the cell might be dequeued instead. It feels very dirty. There must be a better way.
In Apple's Accessory sample the following method is used:
[button addTarget:self action:#selector(checkButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
Then in touch handler touch coordinate retrieved and index path is calculated from that coordinate:
- (void)checkButtonTapped:(id)sender
{
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
...
}
}
I found the method of using the superview's superview to obtain a reference to the cell's indexPath worked perfectly. Thanks to iphonedevbook.com (macnsmith) for the tip link text
-(void)buttonPressed:(id)sender {
UITableViewCell *clickedCell = (UITableViewCell *)[[sender superview] superview];
NSIndexPath *clickedButtonPath = [self.tableView indexPathForCell:clickedCell];
...
}
Here's how I do it. Simple and concise:
- (IBAction)buttonTappedAction:(id)sender
{
CGPoint buttonPosition = [sender convertPoint:CGPointZero
toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
...
}
With Swift 4.2 and iOS 12, you can choose one the 5 following complete examples in order to solve your problem.
#1. Using UIView's convert(_:to:) and UITableView's indexPathForRow(at:)
import UIKit
private class CustomCell: UITableViewCell {
let button = UIButton(type: .system)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button.setTitle("Tap", for: .normal)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.button.addTarget(self, action: #selector(customCellButtonTapped), for: .touchUpInside)
return cell
}
#objc func customCellButtonTapped(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)
guard let indexPath = tableView.indexPathForRow(at: point) else { return }
print(indexPath)
}
}
#2. Using UIView's convert(_:to:) and UITableView's indexPathForRow(at:) (alternative)
This is an alternative to the previous example where we pass nil to the target parameter in addTarget(_:action:for:). This way, if the first responder does not implement the action, it will be send to the next responder in the responder chain until until a proper implementation is found.
import UIKit
private class CustomCell: UITableViewCell {
let button = UIButton(type: .system)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button.setTitle("Tap", for: .normal)
button.addTarget(nil, action: #selector(TableViewController.customCellButtonTapped), for: .touchUpInside)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
return cell
}
#objc func customCellButtonTapped(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)
guard let indexPath = tableView.indexPathForRow(at: point) else { return }
print(indexPath)
}
}
#3. Using UITableView's indexPath(for:) and delegate pattern
In this example, we set the view controller as the delegate of the cell. When the cell's button is tapped, it triggers a call to the appropriate method of the delegate.
import UIKit
protocol CustomCellDelegate: AnyObject {
func customCellButtonTapped(_ customCell: CustomCell)
}
class CustomCell: UITableViewCell {
let button = UIButton(type: .system)
weak var delegate: CustomCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button.setTitle("Tap", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func buttonTapped(sender: UIButton) {
delegate?.customCellButtonTapped(self)
}
}
import UIKit
class TableViewController: UITableViewController, CustomCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.delegate = self
return cell
}
// MARK: - CustomCellDelegate
func customCellButtonTapped(_ customCell: CustomCell) {
guard let indexPath = tableView.indexPath(for: customCell) else { return }
print(indexPath)
}
}
#4. Using UITableView's indexPath(for:) and a closure for delegation
This is an alternative to the previous example where we use a closure instead of a protocol-delegate declaration to handle the button tap.
import UIKit
class CustomCell: UITableViewCell {
let button = UIButton(type: .system)
var buttontappedClosure: ((CustomCell) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button.setTitle("Tap", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
button.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1).isActive = true
button.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func buttonTapped(sender: UIButton) {
buttontappedClosure?(self)
}
}
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.buttontappedClosure = { [weak tableView] cell in
guard let indexPath = tableView?.indexPath(for: cell) else { return }
print(indexPath)
}
return cell
}
}
#5. Using UITableViewCell's accessoryType and UITableViewDelegate's tableView(_:accessoryButtonTappedForRowWith:)
If your button is a UITableViewCell's standard accessory control, any tap on it will trigger a call to UITableViewDelegate's tableView(_:accessoryButtonTappedForRowWith:), allowing you to get the related index path.
import UIKit
private class CustomCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .detailButton
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
return cell
}
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
print(indexPath)
}
}
Found a nice solution to this problem elsewhere, no messing around with tags on the button:
- (void)buttonPressedAction:(id)sender {
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
// do stuff with the indexPath...
}
How about sending the information like NSIndexPath in the UIButton using runtime injection.
1) You need runtime on the import
2) add static constant
3) add NSIndexPath to your button on runtime using:
(void)setMetaData:(id)target withObject:(id)newObj
4) on button press get metadata using:
(id)metaData:(id)target
Enjoy
#import <objc/runtime.h>
static char const * const kMetaDic = "kMetaDic";
#pragma mark - Getters / Setters
- (id)metaData:(id)target {
return objc_getAssociatedObject(target, kMetaDic);
}
- (void)setMetaData:(id)target withObject:(id)newObj {
objc_setAssociatedObject(target, kMetaDic, newObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#On the cell constructor
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
....
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
....
[btnSocial addTarget:self
action:#selector(openComments:)
forControlEvents:UIControlEventTouchUpInside];
#add the indexpath here or another object
[self setMetaData:btnSocial withObject:indexPath];
....
}
#The action after button been press:
- (IBAction)openComments:(UIButton*)sender{
NSIndexPath *indexPath = [self metaData:sender];
NSLog(#"indexPath: %d", indexPath.row);
//Reuse your indexpath Now
}
To do (#Vladimir)'s answer is Swift:
var buttonPosition = sender.convertPoint(CGPointZero, toView: self.tableView)
var indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!
Although checking for indexPath != nil gives me the finger..."NSIndexPath is not a subtype of NSString"
func buttonAction(sender:UIButton!)
{
var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tablevw)
let indexPath = self.tablevw.indexPathForRowAtPoint(position)
let cell: TableViewCell = tablevw.cellForRowAtIndexPath(indexPath!) as TableViewCell
println(indexPath?.row)
println("Button tapped")
}
I would use the tag property like you said, setting the tag like so:
[button setTag:indexPath.row];
then getting the tag inside of the buttonPressedAction like so:
((UIButton *)sender).tag
Or
UIButton *button = (UIButton *)sender;
button.tag;
Though I like the tag way... if you don't want to use tags for whatever reason,
you could create a member NSArray of premade buttons:
NSArray* buttons ;
then create those buttons before rendering the tableView and push them into the array.
Then inside of the tableView:cellForRowAtIndexPath: function you can do:
UIButton* button = [buttons objectAtIndex:[indexPath row] ] ;
[cell.contentView addSubview:button];
Then in the buttonPressedAction: function, you can do
- (void)buttonPressedAction:(id)sender {
UIButton* button = (UIButton*)sender ;
int row = [buttons indexOfObject:button] ;
// Do magic
}
TO HANDLE SECTIONS - I stored the NSIndexPath in a custom UITableViewCell
IN CLKIndexPricesHEADERTableViewCell.xib
IN IB Add UIButton to XIB - DONT add action!
Add outlet #property (retain, nonatomic) IBOutlet UIButton *buttonIndexSectionClose;
DO NOT CTRL+DRAG an action in IB(done in code below)
#interface CLKIndexPricesHEADERTableViewCell : UITableViewCell
...
#property (retain, nonatomic) IBOutlet UIButton *buttonIndexSectionClose;
#property (nonatomic, retain) NSIndexPath * indexPathForCell;
#end
In viewForHeaderInSection (should also work for cellForRow.... etc if you table has only 1 section)
- viewForHeaderInSection is called for each section 1...2...3
- get the cell CLKIndexPricesHEADERTableViewCell
- getTableRowHEADER just does the normal dequeueReusableCellWithIdentifier
- STORE the indexPath IN the UITableView cell
- indexPath.section = (NSInteger)section
- indexPath.row = 0 always (we are only interested in sections)
- (UIView *) tableView:(UITableView *)tableView1 viewForHeaderInSection:(NSInteger)section {
//Standard method for getting a UITableViewCell
CLKIndexPricesHEADERTableViewCell * cellHEADER = [self getTableRowHEADER];
...use the section to get data for your cell
...fill it in
indexName = ffaIndex.routeCode;
indexPrice = ffaIndex.indexValue;
//
[cellHEADER.buttonIndexSectionClose addTarget:self
action:#selector(buttonDELETEINDEXPressedAction:forEvent:)
forControlEvents:UIControlEventTouchUpInside];
cellHEADER.indexPathForCell = [NSIndexPath indexPathForRow:0 inSection:section];
return cellHEADER;
}
USER presses DELETE Button on a Section header and this calls
- (void)buttonDELETEINDEXPressedAction:(id)sender forEvent:(UIEvent *)event
{
NSLog(#"%s", __PRETTY_FUNCTION__);
UIView * parent1 = [sender superview]; // UiTableViewCellContentView
//UIView *myContentView = (UIView *)parent1;
UIView * parent2 = [parent1 superview]; // custom cell containing the content view
//UIView * parent3 = [parent2 superview]; // UITableView containing the cell
//UIView * parent4 = [parent3 superview]; // UIView containing the table
if([parent2 isMemberOfClass:[CLKIndexPricesHEADERTableViewCell class]]){
CLKIndexPricesHEADERTableViewCell *myTableCell = (CLKIndexPricesHEADERTableViewCell *)parent2;
//UITableView *myTable = (UITableView *)parent3;
//UIView *mainView = (UIView *)parent4;
NSLog(#"%s indexPath.section,row[%d,%d]", __PRETTY_FUNCTION__, myTableCell.indexPathForCell.section,myTableCell.indexPathForCell.row);
NSString *key = [self.sortedKeysArray objectAtIndex:myTableCell.indexPathForCell.section];
if(key){
NSLog(#"%s DELETE object at key:%#", __PRETTY_FUNCTION__,key);
self.keyForSectionIndexToDelete = key;
self.sectionIndexToDelete = myTableCell.indexPathForCell.section;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Remove Index"
message:#"Are you sure"
delegate:self
cancelButtonTitle:#"No"
otherButtonTitles:#"Yes", nil];
alertView.tag = kALERTVIEW_REMOVE_ONE_INDEX;
[alertView show];
[alertView release];
//------
}else{
NSLog(#"ERROR: [%s] key is nil for section:%d", __PRETTY_FUNCTION__,myTableCell.indexPathForCell.section);
}
}else{
NSLog(#"ERROR: [%s] CLKIndexPricesHEADERTableViewCell not found", __PRETTY_FUNCTION__);
}
}
In this example I added a Delete button so should show UIAlertView to confirm it
I store the section and key into the dictionary storing info about the section in a ivar in the VC
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if(alertView.tag == kALERTVIEW_REMOVE_ONE_INDEX){
if(buttonIndex==0){
//NO
NSLog(#"[%s] BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
//do nothing
}
else if(buttonIndex==1){
//YES
NSLog(#"[%s] BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
if(self.keyForSectionIndexToDelete != nil){
//Remove the section by key
[self.indexPricesDictionary removeObjectForKey:self.keyForSectionIndexToDelete];
//sort the keys so sections appear alphabetically/numbericsearch (minus the one we just removed)
[self updateTheSortedKeysArray];
//Delete the section from the table using animation
[self.tableView beginUpdates];
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:self.sectionIndexToDelete]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
//required to trigger refresh of myTableCell.indexPathForCell else old values in UITableViewCells
[self.tableView reloadData];
}else{
NSLog(#"ERROR: [%s] OBJECT is nil", __PRETTY_FUNCTION__);
}
}
else {
NSLog(#"ERROR: [%s] UNHANDLED BUTTON:%d", __PRETTY_FUNCTION__,buttonIndex);
}
}else {
NSLog(#"ERROR: [%s] unhandled ALERTVIEW TAG:%d", __PRETTY_FUNCTION__,alertView.tag);
}
}
A better way would be to subclass your button and add a indexPath property to it.
//Implement a subclass for UIButton.
#interface NewButton:UIButton
#property(nonatomic, strong) NSIndexPath *indexPath;
Make your button of type NewButton in the XIB or in the code whereever you are initializing them.
Then in the cellForRowAtIndexPath put the following line of code.
button.indexPath = indexPath;
return cell; //As usual
Now in your IBAction
-(IBAction)buttonClicked:(id)sender{
NewButton *button = (NewButton *)sender;
//Now access the indexPath by buttons property..
NSIndexPath *indexPath = button.indexPath; //:)
}
It works for me aswell, Thanks #Cocoanut
I found the method of using the superview's superview to obtain a reference to the cell's indexPath worked perfectly. Thanks to iphonedevbook.com (macnsmith) for the tip link text
-(void)buttonPressed:(id)sender {
UITableViewCell *clickedCell = (UITableViewCell *)[[sender superview] superview];
NSIndexPath *clickedButtonPath = [self.tableView indexPathForCell:clickedCell];
...
}
you can use the tag pattern:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = #"identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
[cell autorelelase];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 5, 40, 20)];
[button addTarget:self action:#selector(buttonPressedAction:) forControlEvents:UIControlEventTouchUpInside];
[button setTag:[indexPath row]]; //use the row as the current tag
[cell.contentView addSubview:button];
[button release];
}
UIButton *button = (UIButton *)[cell viewWithTag:[indexPath row]]; //use [indexPath row]
[button setTitle:#"Edit" forState:UIControlStateNormal];
return cell;
}
- (void)buttonPressedAction:(id)sender
{
UIButton *button = (UIButton *)sender;
//button.tag has the row number (you can convert it to indexPath)
}
Am I missing something? Can't you just use sender to identify the button. Sender will give you info like this:
<UIButton: 0x4b95c10; frame = (246 26; 30 30); opaque = NO; tag = 104; layer = <CALayer: 0x4b95be0>>
Then if you want to change the properties of the button, say the background image you just tell sender:
[sender setBackgroundImage:[UIImage imageNamed:#"new-image.png"] forState:UIControlStateNormal];
If you need the tag then ACBurk's method is fine.
// how do I know which button sent this message?
// processing button press for this row requires an indexPath.
Pretty straightforward actually:
- (void)buttonPressedAction:(id)sender
{
UIButton *button = (UIButton *)sender;
CGPoint rowButtonCenterInTableView = [[rowButton superview] convertPoint:rowButton.center toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:rowButtonCenterInTableView];
MyTableViewItem *rowItem = [self.itemsArray objectAtIndex:indexPath.row];
// Now you're good to go.. do what the intention of the button is, but with
// the context of the "row item" that the button belongs to
[self performFooWithItem:rowItem];
}
Working well for me :P
if you want to adjust your target-action setup, you can include the event parameter in the method, and then use the touches of that event to resolve the coordinates of the touch. The coordinates still need to be resolved in the touch view bounds, but that may seem easier for some people.
create an nsmutable array and put all button in that array usint[array addObject:yourButton];
in the button press method
-
(void)buttonPressedAction:(id)sender
{
UIButton *button = (UIButton *)sender;
for(int i=0;i<[yourArray count];i++){
if([buton isEqual:[yourArray objectAtIndex:i]]){
//here write wat u need to do
}
}
A slight variation on Cocoanuts answer (that helped me solve this) when the button was in the footer of a table (which prevents you from finding the 'clicked cell':
-(IBAction) buttonAction:(id)sender;
{
id parent1 = [sender superview]; // UiTableViewCellContentView
id parent2 = [parent1 superview]; // custom cell containing the content view
id parent3 = [parent2 superview]; // UITableView containing the cell
id parent4 = [parent3 superview]; // UIView containing the table
UIView *myContentView = (UIView *)parent1;
UITableViewCell *myTableCell = (UITableViewCell *)parent2;
UITableView *myTable = (UITableView *)parent3;
UIView *mainView = (UIView *)parent4;
CGRect footerViewRect = myTableCell.frame;
CGRect rect3 = [myTable convertRect:footerViewRect toView:mainView];
[cc doSomethingOnScreenAtY:rect3.origin.y];
}
I always use tags.
You need to subclass the UITableviewCell and handle the button press from there.
It's simple; make a custom cell and take a outlet of button
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = #"identifier";
customCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
cell.yourButton.tag = indexPath.Row;
- (void)buttonPressedAction:(id)sender
change id in above method to (UIButton *)
You can get the value that which button is being tapped by doing sender.tag.
Subclass the button to store the required value, maybe create a protocol (ControlWithData or something). Set the value when you add the button to the table view cell. In your touch up event, see if the sender obeys the protocol and extract the data. I normally store a reference to the actual object that is rendered on the table view cell.
SWIFT 2 UPDATE
Here's how to find out which button was tapped + send data to another ViewController from that button's indexPath.row as I'm assuming that's the point for most!
#IBAction func yourButton(sender: AnyObject) {
var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(position)
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath!)! as
UITableViewCell
print(indexPath?.row)
print("Tap tap tap tap")
}
For those who are using a ViewController class and added a tableView, I'm using a ViewController instead of a TableViewController so I manually added the tableView in order to access it.
Here is the code for passing data to another VC when tapping that button and passing the cell's indexPath.row
#IBAction func moreInfo(sender: AnyObject) {
let yourOtherVC = self.storyboard!.instantiateViewControllerWithIdentifier("yourOtherVC") as! YourOtherVCVIewController
var position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(position)
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath!)! as
UITableViewCell
print(indexPath?.row)
print("Button tapped")
yourOtherVC.yourVarName = [self.otherVCVariable[indexPath!.row]]
self.presentViewController(yourNewVC, animated: true, completion: nil)
}
Note here i am using custom cell this code is perfectly working for me
#IBAction func call(sender: UIButton)
{
var contentView = sender.superview;
var cell = contentView?.superview as EmployeeListCustomCell
if (!(cell.isKindOfClass(EmployeeListCustomCell)))
{
cell = (contentView?.superview)?.superview as EmployeeListCustomCell
}
let phone = cell.lblDescriptionText.text!
//let phone = detailObject!.mobile!
let url:NSURL = NSURL(string:"tel://"+phone)!;
UIApplication.sharedApplication().openURL(url);
}
Chris Schwerdt's solution but then in Swift worked for me:
#IBAction func rateButtonTapped(sender: UIButton) {
let buttonPosition : CGPoint = sender.convertPoint(CGPointZero, toView: self.ratingTableView)
let indexPath : NSIndexPath = self.ratingTableView.indexPathForRowAtPoint(buttonPosition)!
print(sender.tag)
print(indexPath.row)
}
This problem has two parts:
1) Getting the index path of UITableViewCell which contains pressed UIButton
There are some suggestions like:
Updating UIButton's tag in cellForRowAtIndexPath: method using index path's row value. This is not an good solution as it requires updating tag continuously and it does not work with table views with more than one section.
Adding an NSIndexPath property to custom cell and updating it instead of UIButton's tag in cellForRowAtIndexPath: method. This solves multiple section problem but still not good as it requires updating always.
Keeping a weak refence to parent UITableView in the custom cell while creating it and using indexPathForCell: method to get the index path. Seems a little bit better, no need to update anything in cellForRowAtIndexPath: method, but still requires setting a weak reference when the custom cell is created.
Using cell's superView property to get a reference to parent UITableView. No need to add any properties to the custom cell, and no need to set/update anything on creation/later. But cell's superView depends on iOS implementation details. So it can not be used directly.
But this can be achieved using a simple loop, as we are sure the cell in question has to be in a UITableView:
UIView* view = self;
while (view && ![view isKindOfClass:UITableView.class])
view = view.superview;
UITableView* parentTableView = (UITableView*)view;
So, these suggestions can be combined into a simple and safe custom cell method for getting the index path:
- (NSIndexPath *)indexPath
{
UIView* view = self;
while (view && ![view isKindOfClass:UITableView.class])
view = view.superview;
return [(UITableView*)view indexPathForCell:self];
}
From now on, this method can be used to detect which UIButton is pressed.
2) Informing other parties about button press event
After internally knowing which UIButton is pressed in which custom cell with exact index path, this information needs to be sent to other parties (most probably the view controller handling the UITableView). So, this button click event can be handled in a similar abstraction and logic level to didSelectRowAtIndexPath: method of UITableView delegate.
Two approaches can be used for this:
a) Delegation: custom cell can have a delegate property and can define a protocol. When button is pressed it just performs it's delegate methods on it's delegate property. But this delegate property needs to be set for each custom cell when they are created. As an alternative, custom cell can choose to perform its delegate methods on it's parent table view's delegate too.
b) Notification Center: custom cells can define a custom notification name and post this notification with the index path and parent table view information provided in userInfo object. No need to set anything for each cell, just adding an observer for the custom cell's notification is enough.
I use a solution that subclass UIButton and I thought I should just share it here, codes in Swift:
class ButtonWithIndexPath : UIButton {
var indexPath:IndexPath?
}
Then remember to update it's indexPath in cellForRow(at:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let returnCell = tableView.dequeueReusableCell(withIdentifier: "cellWithButton", for: indexPath) as! cellWithButton
...
returnCell.button.indexPath = IndexPath
returnCell.button.addTarget(self, action:#selector(cellButtonPressed(_:)), for: .touchUpInside)
return returnCell
}
So when responding to the button's event you can use it like
func cellButtonPressed(_ sender:UIButton) {
if sender is ButtonWithIndexPath {
let button = sender as! ButtonWithIndexPath
print(button.indexPath)
}
}