IOS 7 - How to get the indexPath from button placed in UITableViewCell - iphone

I've programmatically created a UITableView and added UISwitch to it's cell accessory view.
This is my code for UISwitch in cell accessory view in cellForRowAtIndexPath method.
UISwitch *accessorySwitch = [[UISwitch alloc]initWithFrame:CGRectZero];
[accessorySwitch setOn:NO animated:YES];
[accessorySwitch addTarget:self action:#selector(changeSwitch:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = accessorySwitch;
This is the method which is called after the button is clicked.
- (void)changeSwitch:(UISwitch *)sender{
UITableViewCell *cell = (UITableViewCell *)[sender superview];
NSIndexPath *indexPath = [self.filterTableView indexPathForCell:cell];
NSLog(#"%ld",(long)indexPath);
……………. My other code…….
}
I am able to print the index path value in iOS 6
But in iOS 7 it prints nil,
Am i missing anything in iOS 7 or there is some other approach to get the indexPath in iOS 7
Thanks,
Arun.

NSLog(#"%ld",(long)indexPath); is wrong this will print the pointer address of indexPath
try using following codes
CGPoint center= sender.center;
CGPoint rootViewPoint = [sender.superview convertPoint:center toView:self.filterTableView];
NSIndexPath *indexPath = [self.filterTableView indexPathForRowAtPoint:rootViewPoint];
NSLog(#"%#",indexPath);

Swift solution:
A UITableView extension like this one can be useful for this.
extension UITableView {
func indexPathForView(view: AnyObject) -> NSIndexPath? {
let originInTableView = self.convertPoint(CGPointZero, fromView: (view as! UIView))
return self.indexPathForRowAtPoint(originInTableView)
}
}
Usage becomes easy everywhere.
let indexPath = tableView.indexPathForView(button)

If you have button in cell. You can get cell by calling superview. And then can get Indexpath by this way.
(void)obButtonTap:(UIButton *)sender {
UITableViewCell *cell = (UITableViewCell *)sender.superview;
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
}

You can assign tags to each switch in cellForRowAtIndexPath method like
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UISwitch *accessorySwitch = [[UISwitch alloc]initWithFrame:CGRectZero];
[accessorySwitch setOn:NO animated:YES];
[accessorySwitch addTarget:self action:#selector(changeSwitch:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = accessorySwitch;
accessorySwitch.tag = indexPath.row;
}
- (void)changeSwitch:(UISwitch *)sender{
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:[sender tag]];
NSLog(#"%ld",(long)indexPath);
}

SWIFT 4 :
extension UITableView {
func indexPathForView(view: AnyObject) -> NSIndexPath? {
let originInTableView = self.convert(CGPoint.zero, from: (view as! UIView))
return self.indexPathForRow(at: originInTableView)! as NSIndexPath
}
}

SWIFT 4 Conversion, creates global extension:
extension UITableView {
func indexPathForView(view: UIView) -> IndexPath? {
let originInTableView = self.convert(CGPoint.zero, from: (view))
return self.indexPathForRow(at: originInTableView)
}
}
Usage example:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ...
cell.myButton.addTarget(self, action: #selector(myButtonAction(_:)), for: .primaryActionTriggered)
return cell
}
#objc func myButtonAction(_ button:UIButton) {
guard let ip = self.tableView.indexPathForView(view: button) else {
return
}
}

You are probably getting burned, making the assumption that the switch's superview is going to be the tableViewCell. Maybe in iOS 7 they changed the hierarchy to achieve some sort of visual effect; maybe there is a new view inserted in there.
You could possibly subclass UISwitch and give it an indexPath property. Then in your cellForRowAtIndexPath, assign it there so you have a reference.

Tested on Swift5.x
if you want to get row number when you click on the button placed in tableViewCell, Then the below function will work.
#IBAction func buttonClickeAction(_ sender: UIButton) {
let btnPoint = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: btnPoint)
print(indexPath?.row)
}

I think that is dangerous to rely on the position of the button to know which one have been clicked.
I prefere create a dictionary taking as a key the button/switch itself, and as a value the indexpath :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
...
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSValue * accessorySwitchKey = [NSValue valueWithNonretainedObject:[cell settingSwitch]];
[[self switchIndexDictionary]setObject:indexPath accessorySwitchKey];
...
}
then when the switch/button is triggered I get the indexpath easily from my dictionary :
- (void)toggleSetting:(id)sender
{
UISwitch *selectedSwitch = (UISwitch *) sender;
NSValue * accessorySwitchKey = [NSValue valueWithNonretainedObject:selectedSwitch];
NSIndexPath *indexPath = [[self switchIndexDictionary]objectForKey: accessorySwitchKey];
}

Related

How to get cell indexpath in uitextfield Delegate Methods?

I have two textfields in a custom cell how to get the indexpath value of Tableview cell in textfield delegate methods I want to get the input value from user and save it to the relavent object. the user can add more cells by clicking button(Add More) in cell..
Thanks in Advance...
Update to iOS7!
With new features in iOS7 now code should be :
UITableViewCell *textFieldRowCell;
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
// Load resources for iOS 6.1 or earlier
textFieldRowCell = (UITableViewCell *) textField.superview.superview;
} else {
// Load resources for iOS 7 or later
textFieldRowCell = (UITableViewCell *) textField.superview.superview.superview;
// TextField -> UITableVieCellContentView -> (in iOS 7!)ScrollView -> Whoola!
}
NSIndexPath *indexPath = [self.tableView indexPathForCell:textFieldRowCell];
A more dynamic solution (no hardcoded superview levels and same code for different iOS versions).
Further, indexPathForCell: will not work if the cell is not visible, therefore I use indexPathForRowAtPoint: as workaround.
//find the UITableViewcell superview
UIView *cell = textField;
while (cell && ![cell isKindOfClass:[UITableViewCell class]])
cell = cell.superview;
//use the UITableViewcell superview to get the NSIndexPath
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:cell.center];
This is how I have been doing it and have been having better luck. I grab the origin of the textField frame. Convert that to a point. Then convert the point to an index path.
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
CGPoint origin = textField.frame.origin;
CGPoint point = [textField.superview convertPoint:origin toView:self.tableView];
NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:point];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
Try this method to get textfield dynamically anywhere from your tableview controller
#pragma mark - Get textfield indexpath
- (NSIndexPath *)TextFieldIndexpath:(UITextField *)textField
{
CGPoint point = [textField.superview convertPoint:textField.frame.origin toView:self.TblView];
NSIndexPath * indexPath = [self.TblView indexPathForRowAtPoint:point];
NSLog(#"Indexpath = %#", indexPath);
return indexPath;
}
To get The indexPath try the following code.
UIView *contentView = (UIVIew *)[textfield superview];
UITableViewCell *cell = (UITableViewCell *)[contentView superview];
if(IS_IOS7_OR_GREATER) {
cell = (UITableViewCell *)[[contentView superview] superview];
}
NSIndexPath *indexPath = [self.tableview indexPathForCell:cell];
Tats it you are done.
To be simple,
NSIndexPath *indexPath = [self.tableview indexPathForCell:(UITableViewCell *)[(UIVIew *)[textfield superview] superview]];
if(IS_IOS7_OR_GREATER) {
cell = (UITableViewCell *)[[[textfield superview] superview] superview];
}
Check the updated answer.
set cell indexpath value to UITextField tag property and you can access the indexpath in delegate methods like textfield.tag
You can set the tags of textfields in cellForRowAtIndexPath: such that it stores info of both cell and text field
For example : If it is cell in 4th row, tag of 1st and 2nd textfields can be 41 and 42 respectively. Similarly, tags of textfields should be 51 and 52 for 5th row and so on...
Then in textfield delegate method, you can get textfield.tag to identify active textfield.
This can be done in the Objective-C runtime for any instance (doesn't have to be UITextField), with any associated object (doesn't have to be NSIndexPath).
For this question, we could create a category UIView+RepresentingIndexPath.
Our interface allows us to set and retrieve an NSIndexPath:
#interface UIView (RepresentingIndexPath)
- (void)representIndexPath:(NSIndexPath *)indexPath;
- (NSIndexPath *)representedIndexPath;
#end
Our implementation uses Objective-C associated objects to set and retrieve an index path on a view:
#import "UIView+RepresentingIndexPath.h"
#import <objc/runtime.h>
static char IndexPathKey;
#implementation UIView (RepresentingIndexPath)
- (void)representIndexPath:(NSIndexPath *)indexPath
{
objc_setAssociatedObject(self, &IndexPathKey, indexPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSIndexPath *)representedIndexPath
{
return objc_getAssociatedObject(self, &IndexPathKey);
}
#end
In action:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TextFieldTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TextFieldCell" forIndexPath:indexPath];
[cell.textField addTarget:self action:#selector(textFieldTextChanged:) forControlEvents:UIControlEventEditingChanged];
[cell.textField representIndexPath:indexPath];
return cell;
}
- (void)textFieldTextChanged:(UITextField *)sender
{
NSIndexPath *indexPath = [sender representedIndexPath];
NSLog(#"%#", indexPath);
}
💣
One final note! Messing around in the runtime should really be avoided if you can achieve what you're trying to do without doing so. Just thought I'd add another solution!
I find this answer searching how can I find the index path of a cell with inside a UITextField.
So, thanks to the answer above, I put my code here, hoping might be usefull.
- (void)searchSelectedIndexPath:(UIView*)view {
// This allow to find selected index path for a table view cell with a text field inside.
for (UIView* subview in view.subviews) {
if ([view isKindOfClass:[UITextField class]]) {
if ([view isFirstResponder]) {
UIView *cell = view;
while (cell && ![cell isKindOfClass:[UITableViewCell class]]) {
cell = cell.superview;
}
self.selectedIndexPath = [self.tableView indexPathForRowAtPoint:cell.center];
return;
}
}
[self searchSelectedIndexPath:subview];
}
}
In this way, when keyboard notification will be raise:
- (void)keyboardDidShow:(NSNotification*)notification {
[self searchSelectedIndexPath:self.tableView];
}
In case somebody like me needs #Kerkness' answer (this one really worked for me) in swift:
func textFieldDidBeginEditing(_ textField: UITextField) {
let origin: CGPoint = textField.frame.origin
let point: CGPoint? = textField.superview?.convert(origin, to: tableView)
let indexPath: IndexPath? = tableView.indexPathForRow(at: point ?? CGPoint.zero)
tableView.scrollToRow(at: indexPath!, at: .middle, animated: true)
}
It should be straight forward enough: you get the point then you get the indexPath and do whatever you need with it!
Thanks, #Luka, it works in a great way.
Here is the swift 4 solution,
var selectedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowHide(_ :)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowHide(_ :)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func searchSelectedIndexPath(view: UIView) {
view.subviews.forEach { (subview) in
if view is UITextView, view.isFirstResponder == true {
var cell:UIView? = view;
while cell != nil && !(cell is UITableViewCell) {
cell = cell?.superview;
}
if cell != nil {
self.selectedIndexPath = self.dashBoardTableView.indexPathForRow(at: (cell?.center)!)
return
}
}
self.searchSelectedIndexPath(view: subview)
}
}
// Keyboard notification observer menthod
#objc fileprivate func keyboardWillShowHide(_ notification: NSNotification){
if notification.name == NSNotification.Name.UIKeyboardWillShow {
UIView.animate(withDuration: duration, animations: { () -> Void in
self.selectedIndexPath = nil;
self.searchSelectedIndexPath(view: self.tableView)
if let indexpath = self.selectedIndexPath {
self.tableView.scrollToRow(at: indexpath, at: .top, animated: false)
} else{
self.bottomContriant.constant = keyboardHeight
self.view.layoutSubviews()
}
})
} else {
self.bottomContriant.constant = 15
UIView.animate(withDuration: duration, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}

How to reach the current selected cell in didSelectRowAtIndexPath?

I want to change the accessory view type of a cell through the method: didSelectRowAtIndexPath , i.e. when a row is selected I want to change the accessory view type, can I do this inside that method ?
You can get the cell from didSelectRowAtIndexPath: method using the indexPath like this.
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
In Swift 3.0 : You can use these 2 ways, according to your need
let cell:UITableViewCell = tableView.cellForRow(at: indexPath) as UITableViewCell
or
let cell:CustomTableViewCell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
Happy Coding !!
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
{
UITableViewCell *myCell = [tableView cellForRowAtIndexPath:indexPath];
// Access accessory View as below.
UIView * myCellAccessoryView = myCell.accessoryView;
}
Use the below function and pass the index path of the selected row so that the particular cell is reloaded again.
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Another solution: store the selected row index and do a reload of tableview. Then in cellForRowAtIndexPath check for the selected row and change the accessory view of the cell.
Swift
You can get the cell from didSelectRowAtIndexPath: method using the indexPath like this:
let cell:UITableViewCell? = tableView.cellForRowAtIndexPath(indexPath)
Store selected indexpath. for eg:
NSInteger selectedIndexPath;
then follow the step below. set your accessory view.
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
AddressTableViewCell *cell=(AddressTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
cell.backgroundColor=[UIColor lightGrayColor];
Address *addr=[self.locationArray objectAtIndex:indexPath.row];
cell.titleLabel.text = addr.addressTitle;
cell.detailLabel.text = addr.addressDetail;
if (indexPath.row == selectedIndexPath) {
[cell.selectButton setImage:[UIImage imageNamed:#"checkboxFilled"] forState:UIControlStateNormal];
}
else {
[cell.selectButton setImage:[UIImage imageNamed:#"checkbox"] forState:UIControlStateNormal];
}
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
selectedIndexPath=indexPath.row;
[self.tableView reloadData];
}

How to uncheck all rows using UITableViewCellAccessoryCheckmark

I've got a UITableView with each row containing a checkbox using UITableViewCellAccessoryCheckmark. I can't figure out how to uncheck all the checkboxes using the didSelectRowAtIndexPath method.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *oldCell;
int count = [self.myTableRowNamesArray count];
for (NSUInteger i = 0; i < count; ++i) {
// Uncheck all checkboxes
// OF COURSE THIS DOESN'T WORK
// BECAUSE i IS AN INTEGER AND INDEXPATH IS A POINTER
FOO: oldCell = [myTableView cellForRowAtIndexPath:(int)i];
// GOOD CODE:
oldCell = [penanceOptionsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
oldCell.accessoryType = UITableViewCellAccessoryNone;
}
UITableViewCell *newCell = [myTableView cellForRowAtIndexPath:indexPath];
newCell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Instead of modifying the .accessoryType of all cells in didSelectRowAtIndexPath:, I suggest storing the selected index in some ivar, and change the .accessoryType in the data source's -tableView:cellForRowAtIndexPath: method, i.e.
-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
self.selectedIndexPath = indexPath;
[tableView reloadData];
}
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
...
cell.accessoryType = [indexPath compare:self.selectedIndexPath] == NSOrderedSame
? UITableViewCellAccessoryCheckmark
: UITableViewCellAccessoryNone;
...
}
With this, only visible cells will be affected, and the million other cells outside of the screen won't need to be modified.
Quite right, here's a full implementation in Swift in the general case of selecting a cell .. you'd use selectedIndexPath elsewhere in the class as you see fit. For example, in cellForRowAtIndexPath to choose the appropriate cell prototype.
// SelectingTableViewController
import UIKit
class SelectingTableViewController: UITableViewController
{
internal var selectedIndexPath:NSIndexPath? = nil
override func viewDidLoad()
{
super.viewDidLoad()
tableView.estimatedRowHeight = 68.0
tableView.rowHeight = UITableViewAutomaticDimension
self.clearsSelectionOnViewWillAppear = false;
}
override func tableView
(tableView:UITableView, didSelectRowAtIndexPath indexPath:NSIndexPath)
{
print("did select....")
// in fact, was this very row selected,
// and the user is clicking to deselect it...
// if you don't want "click a selected row to deselect"
// then on't include this clause.
if selectedIndexPath == indexPath
{
print("(user clicked on selected to deselect)")
selectedIndexPath = nil
tableView.reloadRowsAtIndexPaths(
[indexPath],
withRowAnimation:UITableViewRowAnimation.None)
tableView.deselectRowAtIndexPath(indexPath, animated:false)
return
}
// in fact, was some other row selected??
// user is changing to this row? if so, also deselect that row
if selectedIndexPath != nil
{
let pleaseRedrawMe = selectedIndexPath!
// (note that it will be drawn un-selected
// since we're chaging the 'selectedIndexPath' global)
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths(
[pleaseRedrawMe, indexPath],
withRowAnimation:UITableViewRowAnimation.None)
return;
}
// no previous selection.
// simply select that new one the user just touched.
// note that you can not use Apple's willDeselectRowAtIndexPath
// functions ... because they are freaky
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths(
[indexPath],
withRowAnimation:UITableViewRowAnimation.None)
}
}
for (UITableViewCell *cell in [myTableView visibleCells]) {
cell.accessoryType = UITableViewCellAccessoryNone;
}
But really, you'd be better off just modifying the one cell that actually has the checkmark set. You have to have stored this information somewhere in your model anyway.
You're probably setting some kind of property with this method.
So what i do is:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 1. first unsetting the property
[object someProperty:nil];
// 2. call the reloadData method to uncheck all the checkmarks
[tableView reloadData];
// 3. check the selected cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
// 4. set the checked property
[object setSomeProperty:[indexpath row]];
}
And in my cellForRowAtIndexPath methods i got something like the following code:
if([object someProperty] == [indexpath row]){
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
} else {
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
Yes, cellForRowAtIndexPath: uses NSIndexPath instead of integer so make indexpath by using
indexPathForRow:inSection:
if you are using one section then your loop is fine just pass i in row and 0 for section.

UITableView Multiple Selection

How can I add a UITableView into my View-Based Application where the user will tap on more than one cell, and it will become selected, exactly like the Clock app's "New Alarm" setting named "Repeat" (Clock>Alarms> + >Repeat), and how can I get all of the selected cells in an array?
For multiple selection, add the line below in viewDidLoad()
tableView.allowsMultipleSelection = true
Configure each cell after dequeuing (or initializing) it in tableView(_:cellForRowAt:)
let selectedIndexPaths = tableView.indexPathsForSelectedRows
let rowIsSelected = selectedIndexPaths != nil && selectedIndexPaths!.contains(indexPath)
cell.accessoryType = rowIsSelected ? .checkmark : .none
// cell.accessoryView.hidden = !rowIsSelected // if using a custom image
Update each cell when it's selected/deselected
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .checkmark
// cell.accessoryView.hidden = false // if using a custom image
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .none
// cell.accessoryView.hidden = true // if using a custom image
}
When you're done, get an array of all the selected rows
let selectedRows = tableView.indexPathsForSelectedRows
and get the selected data, where dataArray maps to the rows of a table view with only 1 section
let selectedData = selectedRows?.map { dataArray[$0.row].ID }
In your implementation of -tableView:didSelectRowAtIndexPath: you would set the table view cell's accessoryType property depending on its current value (so it would toggle on and off with multiple taps). For example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)path {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
} else {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
You could either maintain an array of selected states in addition to the cells' own accessory type state, or iterate over the cells in the table view querying for each one's state in order to read out the selected rows.
#BrendanBreg implementation didn't worked for me. #RaphaelOliveira provided good solution, but when you scrolls your table down - wrong rows become selected (because UITableView caches it's cells). So, I've slightly modified Raphael's solution:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
}
/*Here is modified part*/
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
/*
...
Your implementation stays here
we're just adding few lines to make sure
that only correct rows will be selected
*/
if([[tableView indexPathsForSelectedRows] containsObject:indexPath]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
self.tableView.allowsMultipleSelection = YES;
Just a quick tip in addition to the great answer above: to mimic Apple's style from the clock app (making the row select color fade back out after checking / unchecking the row), add this to the didSelectRowAtIndexPath, after the conditionals:
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
From Apple's TableMultiSelect guide.
Here it is what you needed
http://developer.apple.com/library/ios/#samplecode/TableMultiSelect/Introduction/Intro.html#//apple_ref/doc/uid/DTS40011189-Intro-DontLinkElementID_2
I have seen this problem with so many developers. Due to table view's nature of re-using cell it removes or haphazard the checks. I have created a working solution for that. Clone/download code from DevelopmentSupportWorkspace and Execute UITableViewTest project from there.
Here is code summery for that:
#interface CheckBoxTestTableViewController ()
#property (nonatomic, strong) NSArray *dataArray;
#property (nonatomic, strong) NSMutableDictionary *selectedIndexDictionary;
#end
#implementation CheckBoxTestTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
//
_dataArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ImageList" ofType:#"plist"]];
_selectedIndexDictionary = [NSMutableDictionary dictionary];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"checkMarkCell" forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
// Configure the cell...
cell.textLabel.text = _dataArray[indexPath.row][#"text"];
if (_selectedIndexDictionary[indexPath] != nil) cell.accessoryType = UITableViewCellAccessoryCheckmark;
return cell;
}
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
if (_selectedIndexDictionary[indexPath] == nil) {
[_selectedIndexDictionary setObject:_dataArray[indexPath.row] forKey:indexPath];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
}else{
[_selectedIndexDictionary removeObjectForKey:indexPath];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryNone];
}
// [tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
#end
The clock alarms repeat table view is not multiple selection. Think of it as a lit of checkboxes.
When a cell is selected, the font color and accessory type are changed and the selection fades out. When a checked cell is selected, the font color and accessory type are changed back and the selection fades out.
In your didSelectRowAtIndexPath delegate method, you would set the text color and accessory type for the selected cell, then deselect the cell. You would also record the new state in your data model. That could be as simple as a bit mask representing the selected state, but depends on what data you are displaying.
In your cellForRowAtIndexPath: dataSource method, set the text color and accessory type based on your data model.
Actual multiple selection would be similar. You have to keep track of which cells are selected, and set the selected cell of each state as it is created or shown. When the table view reports that a cell is selected, toggle the selection state in your data model and set the selected state of the cell accordingly.
You can't key off indexPath because the cells that refers to changes as you scoll. Put an NSLog in cellForRowAtIndexPath to see that. You can do the check/uncheck in willSelectRowAtIndexPath or didSelectRowAtIndexPath. That covers only the initial check or uncheck though, and will also have things appear as checked once you've scrolled because the underlying cell for a given indexPath changes.
So the solution I found is to have an array of selected things with something that is specific to that given cell, and do the initial check.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
if (![selectedIndexes containsObject:cell.textLabel.text]){
[selectedIndexes addObject:cell.textLabel.text];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
[selectedIndexes removeObject:cell.textLabel.text];
cell.accessoryType = UITableViewCellAccessoryNone;
}
return indexPath;
}
You also have to have logic in cellForRowAtIndexPath to make sure the right stuff is checked or not as the view scrolls:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
cell.accessoryType = UITableViewCellAccessoryNone;
if ([selectedIndexes containsObject:cell.textLabel.text]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
[cell setSelected:YES animated:YES];
}
return cell;
}
1 - Allow multiple selection and toggle of selected state:
tableView.allowsMultipleSelection = true
2 - Collect or get an array of all selected indices when you are done:
let selected = tableView.indexPathsForSelectedRows
Note: This is independent of which cells are currently showing on screen.
3 - Change the appearance of the cell depending on selected state: Override this method in your UITableViewCell subclass. If you don't have a subclass, make one.
// In UITableViewCell subclass
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
accessoryType = selected ? .Checkmark : .None
}
I know that this is an old posting, but for future use, the following code will solve the problem of ensuring that a checkmark will only appear on selected rows during scrolling:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.accessoryType = (cell.isSelected) ? .checkmark : .none
}

How can I keep track of the index path of a button in a table view cell?

I have a table view where each cell has a button accessory view. The table is managed by a fetched results controller and is frequently reordered. I want to be able to press one of the buttons and obtain the index path of that button's table view cell. I've been trying to get this working for days by storing the row of the button in its tag, but when the table gets reordered, the row becomes incorrect and I keep failing at reordering the tags correctly. Any new ideas on how to keep track of the button's cell's index path?
If you feel uncomfortable relying on button.superview, this method should be a little more robust than some of the other answers here:
UIButton *button = (UIButton *)sender;
CGRect buttonFrame = [button convertRect:button.bounds toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonFrame.origin];
This stopped working with iOS 7; check out Mike Weller's answer instead
- (IBAction)clickedButton:(id)sender {
UIButton *button = (UIButton *)sender;
UITableViewCell *cell = (UITableViewCell *)button.superview;
UITableView *tableView = (UITableView *)cell.superview;
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
}
Or shorter:
- (IBAction)clickedButton:(id)sender {
NSIndexPath *indexPath = [(UITableView *)sender.superview.superview indexPathForCell:(UITableViewCell *)sender.superview];
}
Both are untested!
Crawling up view hierarchies with .superview (like all of the existing answers demonstrate) is a really bad way to do things. If UITableViewCell's structure changes (which has happened before) your app will break. Seeing .superview.superview in your code should set off alarm bells.
The button and its handler should be added to a custom UITableViewCell subclass and layed out there. That's where it belongs.
The cell subclass can then delegate out the button event through a standard delegate interface, or a block. You should aim for something like this:
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCustomCell *cell = ...;
// ...
cell.onButtonTapped = ^{
[self buttonSelectedAtIndexPath:indexPath];
}
// OR
cell.delegate = self;
// ...
}
(Note: if you go the block route, you will need to use a __weak self reference to prevent retain cycles, but I thought that would clutter up the example).
If you take the delegate route you would then have this delegate method to implement:
- (void)cellButtonPressed:(UITableViewCell *)cell
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// ...
}
Your code now has full access to the appropriate context when it handles the event.
Implementing this interface on your cell class should be straightforward.
I don't know why I need to call the method superview twice to get the UITableViewCell.
Update:
Thank for Qiulang, now I got it.
"That's because SDK now has added a private class called UITableViewCellContentView for UITableViewCell, which is button's superview now." – Qiulang
UIButton *button = (UIButton *)sender;
UITableViewCell *cell = (UITableViewCell *)button.superview.superview;
UITableView *curTableView = (UITableView *)cell.superview;
NSIndexPath *indexPath = [curTableView indexPathForCell:cell];
I had this same issue also and built a simple recursive method that works no matter how many views deep you triggering control is.
-(NSIndexPath*)GetIndexPathFromSender:(id)sender{
if(!sender) { return nil; }
if([sender isKindOfClass:[UITableViewCell class]])
{
UITableViewCell *cell = sender;
return [self.tableView indexPathForCell:cell];
}
return [self GetIndexPathFromSender:((UIView*)[sender superview])];
}
-(void)ButtonClicked:(id)sender{
NSIndexPath *indexPath = [self GetIndexPathFromSender:sender];
}
I have created one Method for getting indexPath, Hope this will help you.
Create Button Action (aMethod:) in cellForRowAtIndexPath
-(void) aMethod:(UIButton *)sender
{
// Calling Magic Method which will return us indexPath.
NSIndexPath *indexPath = [self getButtonIndexPath:sender];
NSLog(#"IndexPath: %li", indexPath.row);
NSLog(#"IndexRow: %li", indexPath.section);
}
// Here is the Magic Method for getting button's indexPath
-(NSIndexPath *) getButtonIndexPath:(UIButton *) button
{
CGRect buttonFrame = [button convertRect:button.bounds toView:groupTable];
return [groupTable indexPathForRowAtPoint:buttonFrame.origin];
}
Use this Perfect working for me.
CGPoint center= [sender center];
CGPoint rootViewPoint = [[sender superview] convertPoint:center toView:_tableView1];
NSIndexPath *indexPath = [_tableView1 indexPathForRowAtPoint:rootViewPoint];
NSLog(#"%#",indexPath);
SWIFT 2 UPDATE
Here's how to find out which button was tapped
#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")
}