views:

4606

answers:

9

Hi,

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.

A: 

I always use tags.

You need to subclass the UITableviewCell and handle the button press from there.

Chris Beeson
I don't quite understand how. The tag property is set up during the cell creation - this cell is reusable for each row with the same identifier. This tag is specific to the control in a generic reusable cell. How can I use this tag to differentiate buttons in cells which were created in a generic way? Could you post some code?
rein
A: 

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;
ACBurk
This approach is completely broken for tables with sections.
ohhorob
no, you could just use some simple function to put the section in the tag as well.
ACBurk
`tag` is an integer. seems a bit clumsy to be encoding/decoding index paths into view tags.
ohhorob
That's correct, but it is a solution, though not one that I'd use if I had sections. All I was trying to say was that it could be done using this method, that it wasn't broken. A better, more complex version would determine the indexpath from the position of the button inside of the UITableView. However, since rein has said he only has five cells (without sections), it probably makes that method over complicated and your initial comment and this whole comment thread pointless.
ACBurk
A: 

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)
}
Nir Levy
How would I tag the controls if I had multiple controls on a single cell?
rein
I'm not sure this would work - if the cell gets created for row #1 then it will get the tag 1. If it gets dequeued for row #3 then it will still have a tag of 1, not 3.
rein
guess you are right about the second comment. my bad. I think your best solution is to subclass UIButton, add another property or two of your own, and then set/get them in the appropriate cases (stick with the tag:1 you had in your code)
Nir Levy
+4  A: 

I'd also use tags, but when reusing your cell in -cellForRowAtIndexPath method you need to set button's tag each time to the actual row value.
To obtain the button you can iterate through your cell subviews and find it (it may be not optimal way though)

Edit: I've got an idea about may be more elegant methods (have not tested that, but I think it should work):

- (void)buttonPressedAction:(id)sender
{
   UIButton *button = (UIButton *)sender;
   (UITableViewCell*)cell = [button superview];
   int row = [myTable indexPathForCell:cell].row;
}

Or you can set tag for cell each time, but not UIButton. Your action will look then:

- (void)buttonPressedAction:(id)sender
{ 
   UIButton *button = (UIButton *)sender;
   int row = [button superview].tag;
}

Edit2: In Apple's Accessory sample the following method is used: UIButton touch handler set to receive information about touch event:

[button addTarget:self action:@selector(checkButtonTapped:event:) forControlEvents:UIControlEventTouchUpInside];

Then in touch handler touch coordinate retrieved and index path is calculated from that coordinate:

- (void)checkButtonTapped:(id)sender event:(id)event
{
    NSSet *touches = [event allTouches];
    UITouch *touch = [touches anyObject];
    CGPoint currentTouchPosition = [touch locationInView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
    if (indexPath != nil)
    {
     ...
    }
}
Vladimir
Yeah this is what I settled on (see my edit). I agree with you that it's not optimal.
rein
This approach is not too reliable, as you will have to rely on the assumption of how 'nested' the button is within the `UITableViewCell`
ohhorob
But you add UIButton to UITableViewCell yourself so you must just be consistent with what you do when creating cell. Although this approach does not really look elegant I have to admit
Vladimir
For the first solution, you will need to grab [[button superview] superview] since the first superview call will give you the contentView, and finally the second will give you the UITableViewCell. The second solution doesn't work well if you are adding/removing cells since it will invalidate the row index. Therefore, I went with the first solution as outlined and it worked perfect.
raidfive
For the 1st solution-yes if you add button to the cell's contentview you will need to call superview twice (in my app I added button to the cell directly - that's why I called it once). In the second solution you will need to update cell's tag each time you reuse it - I mentioned it in the answer. 3rd solution may looks not so straightforward but apple used it in its sample so it may be the most correct one...
Vladimir
This will reliably pick out the cell that owns the button:UIView *view = button; while (![view isKindOfClass:[UITableViewCell class]]){ view = [view superview]}
Jacob Lyles
+1  A: 

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
}
Eld
+2  A: 

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...
}
Alpinista
Very cool solution - I like it.
rein
It's not clear in this example where you get the 'event' object from.
Nick Ludlam
This is the solution I went with. Using tags is unpredictable when adding/removing rows since their indexes change. Also,
raidfive
+5  A: 

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

}
Cocoanut
You great man. I tried your code. It worked perfect for me. Thank You.
srikanth rongali
A: 

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.

Michael
They're looking for their "object" that the button relates to
ohhorob
A: 
// 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.

ohhorob