July 6, 2009

Changing the Accessory View

In my application, I want to replace the button in the accessory view area of a table cell with my own button. I want it to look like this:
Clicking on the cell should produce the callback tableView:(UITableView *) aTableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath. However, when the pencil icon is hit, I want it to do a different callback. I didn't want to make a custom table cell class, and from reading the documentation, it appeared relatively straight forward to use the existing table cell class. All I had to do was set the cell property accessoryView to something else. Here is what the SDK documentation says:
@property(nonatomic, retain) UIView *accessoryView Discussion If the value of this property is not nil, the UITableViewCell class uses the given view for the accessory view and ignores the value of the accessoryType property. The provided accessory view can be a framework-provided control or label or a custom view. The accessory view appears in the the right side of the cell. If the cell is enabled (through the UIView property userInteractionEnabled) , the accessory view tracks touches and, if tapped, sends the accessory action message set through the accessoryAction property.
Ok, sounds simple enough, so I went ahead and coded up what I thought was a trivial solution. I picked a UIImageView to put into the accessoryView. I was careful about setting the userInteractionEnabled flag and everything. Here is the snippet of code I came up with (which belongs in the cellForRowAtIndexPath method, in the case we have to create a new UITableCell object):
// stick the pencil icon on the accessory view
UIImageView* pencil = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon-pencil.gif"]];
pencil.userInteractionEnabled = YES;
pencil.frame = CGRectMake(0, 0, 15, 15);
cell.accessoryView = pencil;
cell.accessoryAction = @selector(didTapEditButton:); 
Well, this didn't work. All taps on the image produced the didSelectRowAtIndexPath callback. I re-read the documentation on accessoryView. That last sentence mentions the accessoryAction property, so I thought I should take a peek at that:
@property(nonatomic) SEL accessoryAction Discussion If you specify a selector for the accessory action, a message is sent only if the accessory view is a detail disclosure button—that is, the cell's accessoryType property is assigned a value ofUITableViewCellAccessoryDetailDisclosureButton. If the value of this property is NULL, no action message is sent. The accessory view is a UITableViewCell-defined control, framework control, or custom control on the right side of the cell. It is often used to display a new view related to the selected cell. If the accessory view inherits from UIControl, you may set a target and action through the addTarget:action:forControlEvents: method. See accessoryView for more information.
Hmmm. That very first sentence says I can't do this with a custom view. But the next paragraph states I can just use the addTarget:action:forControlEvents: method. (Then it makes a circular reference back to accessoryView for more info, which doesn't offer more info.) But a UIImageView is not derived from UIControl, so that is not available. The solution to this mess is to use a UIButton instead of a UIImageView. UIButtons are UIControls, so the final code looks like this:
// stick the pencil icon on the accessory view
UIButton* pencil = [UIButton buttonWithType:UIButtonTypeCustom];
[pencil setImage:[UIImage imageNamed:@"icon-pencil.gif"] forState:UIControlStateNormal];
pencil.frame = CGRectMake(0, 0, 15, 15);
pencil.userInteractionEnabled = YES;
[pencil addTarget:self action:@selector(didTapEditButton:) forControlEvents:UIControlEventTouchDown];
cell.accessoryView = pencil; 
This time it works. I get the desired callback when the pencil icon is touched. The summary is: use a UIControl based widget in the accessoryView, and ignore the accessoryAction property.


  1. thanks for the nice article. I want to call delegate
    in action
    [cellInfo addTarget:self action:@selector(tableView: accessoryButtonTappedForRowWithIndexPath:) forControlEvents:UIControlEventTouchDown];

    deleage is called but with nil parameters so how I can pass parameters in @selector any other way to do this?

  2. You have to provide a callback method that adheres to how the button press event is going to call you. Most UIControls, if not all, call you this way:

    - (void) someAction:(id) sender

    In other words, they are only going to send you 1 parameter, the UIControl itself.

    You will have to implement this action method and call the tableView method yourself from there.