在UITableView中检测哪个UIButton被按下

217

我有一个带有5个UITableViewCellsUITableView。每个单元格都包含一个设置如下的UIButton:

- (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;
}

我的问题是:在buttonPressedAction:方法中,我如何知道哪个按钮已被按下。我考虑使用标签,但我不确定这是否是最佳路线。我希望能够将indexPath标记到控件上。

- (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. 
}

如何标准地完成这个任务?

编辑:

我通过以下方式已经解决了这个问题,但仍希望知道是否存在更好的方法。

- (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;
}

需要注意的是,我无法在创建单元格时设置标记,因为该单元格可能被取消排队。这感觉很不好。一定有更好的方法。


我认为使用您的标签解决方案没有任何问题。由于单元格是可重用的,因此按照您在此处执行的方式将标签设置为行索引是有意义的。我认为这比将触摸位置转换为行索引更加优雅。 - Erik van der Neut
26个回答

401

在苹果的Accessory示例中,使用了以下方法:

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

然后在触摸处理程序中检索触摸坐标,并从该坐标计算出索引路径:

- (void)checkButtonTapped:(id)sender
{
    CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
    if (indexPath != nil)
    {
     ...
    }
}

2
但是你自己将UIButton添加到UITableViewCell中,因此在创建单元格时必须保持一致。尽管这种方法看起来并不太优雅,但我不得不承认。 - Vladimir
1
对于第一个解决方案,您需要获取[[button superview] superview],因为第一个superview调用将为您提供contentView,最后一个将为您提供UITableViewCell。如果您正在添加/删除单元格,则第二个解决方案效果不佳,因为它会使行索引无效。因此,我选择了第一个解决方案,并且它完美地工作了。 - raidfive
3
这段代码可靠地选择拥有按钮的单元格:UIView *view = button; while (![view isKindOfClass:[UITableViewCell class]]){ view = [view superview]} - Jacob Lyles
@Vladimir:太棒了!谢谢。人们不应该忘记“self.tableView”中的“self”...这让我卡了两个小时。 - Armand
1
当使用[button addTarget:self action:@selector(checkButtonTapped:) forControlEvents:UIControlEventTouchUpInside];时,存在一个陷阱,因为addTarget:action:forControlEvents:会在滚动表格时添加多个重复的目标和操作,它不会删除先前的目标和操作,所以当您单击按钮时,方法checkButtonTapped:将被调用多次。最好在添加它们之前删除目标和操作。 - bandw
显示剩余10条评论

48

我发现使用父视图的父视图来获取单元格的indexPath的方法非常完美。感谢iphonedevbook.com(macnsmith)提供的提示链接文字

-(void)buttonPressed:(id)sender {
 UITableViewCell *clickedCell = (UITableViewCell *)[[sender superview] superview];
 NSIndexPath *clickedButtonPath = [self.tableView indexPathForCell:clickedCell];
...

}

如果你(Stackoverflow读者)尝试这个方法,但它对你不起作用,请检查一下你的实现中是否你的UIButton实际上是你的UITableViewCell的孙子。在我的实现中,我的UIButton是我的UITableViewCell的直接子级,所以我需要在Cocoanut的代码中去掉一个“superview”,然后它就可以工作了。 - Jon Schneider
29
这真的非常、非常错误,而且在新版本的操作系统中已经出现问题。不要遍历你没有所有权的 superview 树。 - Kenrik March
在Storyboard中,对于原型单元格,通过添加第三个调用superview:[[[sender superview] superview] superview],这对我有效。 - Sam Brodkin
2
这在iOS 6下对我起作用,但在iOS 7中失效了。 @KenrikMarch似乎有一个有效的观点! - Jon Schneider
3
在iOS 7中,它向上遍历父视图需要多一步。例如:[[[sender superview] superview] superView]; - CW0007007
显示剩余2条评论

43

这是我的方法。简单明了:

- (IBAction)buttonTappedAction:(id)sender
{
    CGPoint buttonPosition = [sender convertPoint:CGPointZero
                                           toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
    ...
}

2
更加简单的方法:使用 CGPointZero 替代 CGPointMake(0, 0) ;-) - Jakob W
易于使用。此外,易于将其翻译为Swift 3。你是最棒的 :) - Francisco Romero
将此转换为Swift,下面是我能找到的最简单的解决方案。谢谢Chris! - Rutger Huijsmans

8

在Swift 4.2和iOS 12中,您可以选择以下5个完整示例之一来解决问题。


#1. 使用UIViewconvert(_:to:)UITableViewindexPathForRow(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. 使用 UIViewconvert(_:to:)UITableViewindexPathForRow(at:) 方法(替代方案)

这是一个替代方案,用于解决在 addTarget(_:action:for:) 中将 target 参数设置为 nil 的问题。通过这种方式,如果第一响应者没有实现该操作,则会将其发送到响应者链中的下一个响应者,直到找到适当的实现。

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. 使用UITableViewindexPath(for:)和委托模式

在本例中,我们将视图控制器设置为单元格的委托。当单元格的按钮被点击时,它会触发委托的适当方法的调用。

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. 使用 UITableViewindexPath(for:) 和闭包来进行委托

这是一个替代先前示例的方法,我们使用闭包来处理按钮点击,而不是协议-委托声明。

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. 使用UITableViewCellaccessoryTypeUITableViewDelegatetableView(_:accessoryButtonTappedForRowWith:)

如果您的按钮是UITableViewCell的标准附加控件,任何对其的点击都会触发调用UITableViewDelegatetableView(_:accessoryButtonTappedForRowWith:)方法,允许您获取相关的索引路径。

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)
    }

}

6

我在其他地方找到了一个很好的解决方案,不需要对按钮上的标签进行任何操作:

- (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...
}

5
在这个例子中,不清楚你从哪里获取'event'对象。 - Nick Ludlam
这是我采用的解决方案。使用标签在添加/删除行时是不可预测的,因为它们的索引会发生变化。 - raidfive
@NickLudlam№╝џтЈ»УЃйТќ╣Т│ЋтљЇСИЇТў»buttonPressedAction:УђїТў»buttonPressedAction:forEvent:сђѓ - KPM

5
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")
    }

5

要实现(@Vladimir)的答案,需要使用Swift:

var buttonPosition = sender.convertPoint(CGPointZero, toView: self.tableView)
var indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!

尽管检查indexPath != nil会给我返回"NSIndexPath不是NSString的子类型"的错误信息...


5
如何使用运行时注入在UIButton中发送类似NSIndexPath的信息。
1)导入所需的运行时库
2)添加静态常量
3)使用以下方法将NSIndexPath在运行时添加到您的按钮中:
(void)setMetaData:(id)target withObject:(id)newObj
4)在按钮按下时使用以下方法获取元数据:
(id)metaData:(id)target
享受吧!
    #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
    }

1
如果表被重新排列或删除行,则这将不起作用。 - Neil

3
我会按照你说的那样使用标签属性,设置标签如下所示:
[button setTag:indexPath.row];

然后在buttonPressedAction中获取标签,如下所示:

((UIButton *)sender).tag

或者

UIButton *button = (UIButton *)sender; 
button.tag;

5
对于带有章节的表格,这种方法完全失效。 - ohhorob
不,你可以使用一些简单的函数将该部分放入标签中。 - ACBurk
2
tag是一个整数。将索引路径编码/解码为视图标签似乎有些笨拙。 - ohhorob
没错,但这是一种解决方案,尽管如果我有章节的话,我不会使用它。我想说的是,可以使用这种方法来完成,而且它并没有出现问题。更好、更复杂的版本将从UITableView内部按钮的位置确定indexpath。然而,由于rein说他只有五个单元格(没有章节),所以这种方法可能过于复杂,你最初的评论和整个评论线都变得毫无意义。 - ACBurk

3

虽然我喜欢标签的方式...但如果由于某种原因您不想使用标签,您可以创建一个预制按钮的成员NSArray

NSArray* buttons ;

在渲染tableView之前创建这些按钮并将它们推入数组中。

然后在tableView:cellForRowAtIndexPath:函数内部可以这样做:

UIButton* button = [buttons objectAtIndex:[indexPath row] ] ;
[cell.contentView addSubview:button];

然后在buttonPressedAction:函数中,你可以进行以下操作。
- (void)buttonPressedAction:(id)sender {
   UIButton* button = (UIButton*)sender ;
   int row = [buttons indexOfObject:button] ;
   // Do magic
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接