Using a custom image for a UITableViewCell's accessoryView and having it respond to UITableViewDelegate
Asked Answered
P

11

143

I'm using a custom drawn UITableViewCell, including the same for the cell's accessoryView. My setup for the accessoryView happens by the way of something like this:

UIImage *accessoryImage = [UIImage imageNamed:@"accessoryDisclosure.png"];
UIImageView *accImageView = [[UIImageView alloc] initWithImage:accessoryImage];
accImageView.userInteractionEnabled = YES;
[accImageView setFrame:CGRectMake(0, 0, 28.0, 28.0)];
self.accessoryView = accImageView;
[accImageView release];

Also when the cell is initialized, using initWithFrame:reuseIdentifier: I ensured to set the following property:

self.userInteractionEnabled = YES;

Unfortunately in my UITableViewDelegate, my tableView:accessoryButtonTappedForRowWithIndexPath: method (try repeating that 10 times) is not getting triggered. The delegate is definitely wired up properly.

What can be possibly missing?

Thanks all.

Polly answered 15/5, 2009 at 15:40 Comment(0)
H
229

Sadly that method doesn't get called unless the internal button type provided when you use one of the predefined types is tapped. To use your own, you'll have to create your accessory as a button or other UIControl subclass (I'd recommend a button using -buttonWithType:UIButtonTypeCustom and setting the button's image, rather than using a UIImageView).

Here's some things I use in Outpost, which customizes enough of the standard widgets (just slightly, to match our teal colouring) that I wound up doing my own UITableViewController intermediary subclass to hold utility code for all other table views to use (they now subclass OPTableViewController).

Firstly, this function returns a new detail disclosure button using our custom graphic:

- (UIButton *) makeDetailDisclosureButton
{
    UIButton * button = [UIButton outpostDetailDisclosureButton];

[button addTarget: self
               action: @selector(accessoryButtonTapped:withEvent:)
     forControlEvents: UIControlEventTouchUpInside];

    return ( button );
}

The button will call this routine when it's done, which then feeds the standard UITableViewDelegate routine for accessory buttons:

- (void) accessoryButtonTapped: (UIControl *) button withEvent: (UIEvent *) event
{
    NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint: [[[event touchesForView: button] anyObject] locationInView: self.tableView]];
    if ( indexPath == nil )
        return;

    [self.tableView.delegate tableView: self.tableView accessoryButtonTappedForRowWithIndexPath: indexPath];
}

This function locates the row by getting the location in the table view of a touch from the event provided by the button and asking the table view for the index path of the row at that point.

Holmen answered 15/5, 2009 at 16:25 Comment(10)
Thanks Jim. That's a shame I spent more than 20 minutes wondering why I can't do it with a custom imageView. I just saw how to do this on Apple's sample Accessory app. Your answer is well explained and documented though so I'm marking it up and keeping it around. Thanks again. :-)Polly
Jim, great answer. One potential issue (at least on my end) - I had to add the following line to get the touches to register on the button: button.userInteractionEnabled = YES;Deguzman
Just for others looking at this answer, you could also just put a tag on the button which corresponds to the row (if you have multiple sections, you'll need to do some math) and then just pull the tag out from the button in the function. I think it might be a little faster than calculating the touch.Restrict
this requires you to hard-code the self.tableView. what if you don't know which tableview contains the row?Centripetal
Go up the view chain starting at button.superview until you find a UITableView instance.Holmen
Weird get error [UIButton outpostDetailDisclosureButton] cannot be found. Using iPhone SDK 5.1.1 Xcode 4.4Priam
That's not a built-in method; you have to implement a 'create a button' method of your own.Holmen
Works for me as well. If you are using storyboards and seques for navigating, the method handling the tap should be more like this: {code} - (void) accessoryButtonTapped: (UIControl *) button withEvent: (UIEvent *) event { [self performSegueWithIdentifier:@"segueName" sender:self]; } {code}Connell
@Restrict I used to think that doing a hitTest is overkill and tags will suffice. I have in fact used the tags idea in some of my code. But today I encountered a problem where the user can add new rows. This kills the hack using tags. The solution suggested by Jim Dovey (and as seen in Apple's sample code) is a generic solution and works in all situationsFrodi
@Frodi - Great point. Thanks for pointing that out today. I'm just about to refactor the code where I did that.Restrict
T
77

I found this website to be very helpful: custom accessory view for your uitableview in iphone

In short, use this in cellForRowAtIndexPath::

UIImage *image = (checked) ? [UIImage imageNamed:@"checked.png"] : [UIImage imageNamed:@"unchecked.png"];

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
CGRect frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
button.frame = frame;
[button setBackgroundImage:image forState:UIControlStateNormal];

[button addTarget:self action:@selector(checkButtonTapped:event:)  forControlEvents:UIControlEventTouchUpInside];
button.backgroundColor = [UIColor clearColor];
cell.accessoryView = button;

then, implement this method:

- (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)
    {
        [self tableView: self.tableView accessoryButtonTappedForRowWithIndexPath: indexPath];
    }
}
Troubadour answered 6/10, 2010 at 20:3 Comment(3)
I'd say +1 for this as it's what Apple recommends doing in their sample code in their docs: developer.apple.com/library/ios/#samplecode/Accessory/Listings/…Wardell
Setting the frame was the missing piece for me. You can also just setImage (instead of background) as long as you don't also want any text.Tracietracing
The link has broken in @richarddas's answer. New Link : developer.apple.com/library/prerelease/ios/samplecode/Accessory/…Snorkel
G
7

My approach is to create a UITableViewCell subclass and encapsulate the logic that will call the usual UITableViewDelegate's method within it.

// CustomTableViewCell.h
@interface CustomTableViewCell : UITableViewCell

- (id)initForIdentifier:(NSString *)reuseIdentifier;

@end

// CustomTableViewCell.m
@implementation CustomTableViewCell

- (id)initForIdentifier:(NSString *)reuseIdentifier;
{
    // the subclass specifies style itself
    self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
    if (self) {
        // get the button elsewhere
        UIButton *accBtn = [ViewFactory createTableViewCellDisclosureButton];
        [accBtn addTarget: self
                   action: @selector(accessoryButtonTapped:withEvent:)
         forControlEvents: UIControlEventTouchUpInside];
        self.accessoryView = accBtn;
    }
    return self;
}

#pragma mark - private

- (void)accessoryButtonTapped:(UIControl *)button withEvent:(UIEvent *)event
{
    UITableViewCell *cell = (UITableViewCell*)button.superview;
    UITableView *tableView = (UITableView*)cell.superview;
    NSIndexPath *indexPath = [tableView indexPathForCell:cell];
    [tableView.delegate tableView:tableView accessoryButtonTappedForRowWithIndexPath:indexPath];
}

@end
Geld answered 4/5, 2012 at 16:55 Comment(1)
This is the BEST answer. But button.superview, cell.superview and [tableView.delegate tableView:...] are not safe enough.Mcdonnell
I
3

An extension to Jim Dovey's answer above:

Be careful when you use a UISearchBarController with your UITableView. In that case you want to check for self.searchDisplayController.active and use self.searchDisplayController.searchResultsTableViewinstead of self.tableView. Otherwise you'll get unexpected results when the searchDisplayController is active, especially when the search results are scrolled.

For example:

- (void) accessoryButtonTapped:(UIControl *)button withEvent:(UIEvent *)event
{
    UITableView* tableView = self.tableView;
    if(self.searchDisplayController.active)
        tableView = self.searchDisplayController.searchResultsTableView;

    NSIndexPath * indexPath = [tableView indexPathForRowAtPoint:[[[event touchesForView:button] anyObject] locationInView:tableView]];
    if(indexPath)
       [tableView.delegate tableView:tableView accessoryButtonTappedForRowWithIndexPath:indexPath];
}
Insubstantial answered 2/5, 2013 at 11:12 Comment(0)
M
2
  1. Define a macro for tags of buttons:

    #define AccessoryViewTagSinceValue 100000 // (AccessoryViewTagSinceValue * sections + rows) must be LE NSIntegerMax
    
  2. Create button and set the cell.accessoryView when creating a cell

    UIButton *accessoryButton = [UIButton buttonWithType:UIButtonTypeContactAdd];
    accessoryButton.frame = CGRectMake(0, 0, 30, 30);
    [accessoryButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
    cell.accessoryView = accessoryButton;
    
  3. Set cell.accessoryView.tag by indexPath in UITableViewDataSource method -tableView:cellForRowAtIndexPath:

    cell.accessoryView.tag = indexPath.section * AccessoryViewTagSinceValue + indexPath.row;
    
  4. Event handler for buttons

    - (void) accessoryButtonTapped:(UIButton *)button {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:button.tag % AccessoryViewTagSinceValue
                                                    inSection:button.tag / AccessoryViewTagSinceValue];
    
        [self.tableView.delegate tableView:self.tableView accessoryButtonTappedForRowWithIndexPath:indexPath];
    }
    
  5. Implement the UITableViewDelegate method

    - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
        // do sth.
    }
    
Mich answered 11/7, 2012 at 13:45 Comment(1)
Nobody should use tag unless when absolute necessary, look up other solution.Painterly
H
2

When the button is tapped, you could have it call the following method inside a UITableViewCell subclass

 -(void)buttonTapped{
     // perform an UI updates for cell

     // grab the table view and notify it using the delegate
     UITableView *tableView = (UITableView *)self.superview;
     [tableView.delegate tableView:tableView accessoryButtonTappedForRowWithIndexPath:[tableView indexPathForCell:self]];

 }
Hazelwood answered 22/2, 2013 at 6:30 Comment(0)
V
1

You must use a UIControl to properly get event dispatch (for instance a UIButton) instead of simple UIView/UIImageView.

Valaria answered 5/10, 2010 at 21:41 Comment(0)
L
1

With yanchenko approach I had to add: [accBtn setFrame:CGRectMake(0, 0, 20, 20)];

If you're using xib file to customise your tableCell then initWithStyle:reuseIdentifier: wont get called.

Instead override:

-(void)awakeFromNib
{
//Put your code here 

[super awakeFromNib];

}
Labio answered 2/7, 2012 at 18:30 Comment(0)
C
1

Swift 5

This approach uses the UIButton.tag to store the indexPath using basic bit-shifting. The approach will work on 32 & 64 bit systems as long as you don't have more than 65535 sections or rows.

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "cellId")
    let accessoryButton = UIButton(type: .custom)
    accessoryButton.setImage(UIImage(named: "imageName"), for: .normal)
    accessoryButton.sizeToFit()
    accessoryButton.addTarget(self, action: #selector(handleAccessoryButton(sender:)), for: .touchUpInside)

    let tag = (indexPath.section << 16) | indexPath.row
    accessoryButton.tag = tag
    cell?.accessoryView = accessoryButton

}

@objc func handleAccessoryButton(sender: UIButton) {
    let section = sender.tag >> 16
    let row = sender.tag & 0xFFFF
    // Do Stuff
}
Compromise answered 5/6, 2019 at 17:45 Comment(0)
L
0

As of iOS 3.2 you can avoid the buttons that others here are recommending and instead use your UIImageView with a tap gesture recognizer. Be sure to enable user interaction, which is off by default in UIImageViews.

Lepton answered 3/11, 2014 at 19:33 Comment(0)
M
0

With a very useful extension:

public extension UIResponder {
    func next<T>(of type: T.Type) -> T? {
        return (next as? T) ?? next?.next(of: type)
    }
    func closest<T>(of type: T.Type) -> T? {
        return (self as? T) ?? next?.closest(of: type)
    }
}

Set cell.accessoryView with a UIButton:

let button = UIButton(frame: CGRect(x: 0.0, y: 0.0, width: 26.0, height: 26.0), primaryAction: UIAction { action in
    if let button = action.sender as? UIButton,
       let cell = button.next(of: UITableViewCell.self),
       let tableView = cell.next(of: UITableView.self),
       let indexPath = tableView.indexPath(for: cell) {
        tableView.delegate?.tableView?(tableView, accessoryButtonTappedForRowWith: indexPath)
    }
})
let image = UIImage(systemName: "chevron.forward.circle")?
    .withTintColor(.systemGray, renderingMode: .alwaysOriginal)
    .applyingSymbolConfiguration(UIImage.SymbolConfiguration(pointSize: UIFont.systemFontSize + 2.0, weight: .light, scale: .large))
button.setImage(image, for: .normal)
cell.accessoryView = button

Then these two delegate methods will be called when tap on cell or cell.accessoryView respectively:

extension YourTableViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        print("didSelectRowAt: \(indexPath)")
    }
    
    func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        print("accessoryButtonTappedForRowWith: \(indexPath)")
    }
}
Mich answered 4/2 at 16:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.