You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS 11中TableView单元格按钮的订阅/取消订阅点击事件失效

这个问题我之前做Xamarin.iOS项目时也碰到过,核心就是TableView的单元格复用机制在搞鬼!咱们一步步理清楚问题出在哪,再给你几个靠谱的解决办法。

为什么会出现点击多次触发的情况?

  • 当你刷新TableView数据时,系统会复用已经创建过的单元格,而不是每次都新建。如果你的事件绑定逻辑只在GetCell里做「先减再加」,但某次复用的时候,按钮上可能已经绑定了不止一次事件(比如之前的移除操作没生效,或者旧ViewController实例的事件还被按钮持有),每次复用又加一次,自然点一下触发多次。
  • 另外,当你关闭再重新打开ViewController时,旧的ViewController实例可能还没被GC回收,而之前的单元格还持有它的事件引用,新的ViewController又绑定了一次,点击就会同时触发新旧实例的事件。

解决方案1:在单元格的PrepareForReuse里统一清理(最推荐)

这是苹果官方推荐的处理单元格复用的方式,PrepareForReuse会在单元格被复用时自动调用,咱们在这里把事件、Tag这些状态都重置掉:

首先自定义你的单元格:

public class CustomCell : UITableViewCell
{
    public UIButton btn_Click { get; private set; }
    // 用自定义事件把点击事件传递给ViewController,避免直接绑定VC的方法
    public event EventHandler<int> OnButtonClicked;

    public CustomCell(NSString reuseId) : base(UITableViewCellStyle.Default, reuseId)
    {
        // 初始化按钮
        btn_Click = new UIButton(UIButtonType.System)
        {
            Frame = new CGRect(20, 10, 120, 35)
        };
        // 只在单元格初始化时绑定一次内部的点击处理方法
        btn_Click.TouchUpInside += HandleButtonTap;
        ContentView.AddSubview(btn_Click);
    }

    private void HandleButtonTap(object sender, EventArgs e)
    {
        // 把点击事件传递出去,带上当前的行号Tag
        OnButtonClicked?.Invoke(this, btn_Click.Tag);
    }

    public override void PrepareForReuse()
    {
        base.PrepareForReuse();
        // 重置Tag,避免复用后带旧的行号
        btn_Click.Tag = -1;
        // 移除ViewController之前绑定的事件,避免重复绑定
        OnButtonClicked = null;
    }
}

然后在你的ViewController的GetCell方法里:

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell("CustomCell") as CustomCell;
    if (cell == null)
    {
        cell = new CustomCell(new NSString("CustomCell"));
    }

    // 绑定自定义事件,处理点击逻辑
    cell.OnButtonClicked += (s, row) =>
    {
        // 这里写你的点击处理代码,row就是对应的行号
        Console.WriteLine($"点击了第{row}行的按钮");
    };
    // 设置当前行号为Tag
    cell.btn_Click.Tag = indexPath.Row;

    // 其他单元格内容设置...

    return cell;
}

解决方案2:用Command模式(适合MVVM架构)

如果你的项目用了MVVM,这种方式更优雅,不需要手动处理事件订阅:

修改自定义单元格:

public class CustomCell : UITableViewCell
{
    public UIButton btn_Click { get; private set; }
    public ICommand ButtonCommand { get; set; }
    public object CommandParam { get; set; }

    public CustomCell(NSString reuseId) : base(UITableViewCellStyle.Default, reuseId)
    {
        btn_Click = new UIButton(UIButtonType.System)
        {
            Frame = new CGRect(20, 10, 120, 35)
        };
        btn_Click.TouchUpInside += (s, e) => ButtonCommand?.Execute(CommandParam);
        ContentView.AddSubview(btn_Click);
    }

    public override void PrepareForReuse()
    {
        base.PrepareForReuse();
        // 重置Command和参数
        ButtonCommand = null;
        CommandParam = null;
    }
}

然后在ViewController的GetCell里:

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell("CustomCell") as CustomCell;
    if (cell == null)
    {
        cell = new CustomCell(new NSString("CustomCell"));
    }

    // 绑定Command和行号参数
    cell.ButtonCommand = new Command(() => HandleButtonTap(indexPath.Row));
    cell.CommandParam = indexPath.Row;

    return cell;
}

private void HandleButtonTap(int row)
{
    // 处理点击逻辑
}

解决方案3:修复你现有代码的问题(最小改动)

如果你不想改单元格结构,那就在ViewController销毁时主动清理所有单元格的事件绑定,同时保持GetCell里的逻辑:

首先在ViewController里添加清理逻辑:

public override void ViewWillDisappear(bool animated)
{
    base.ViewWillDisappear(animated);
    // 遍历所有可见单元格,移除事件绑定
    foreach (var cell in tableView.VisibleCells)
    {
        var customCell = cell as CustomCell;
        customCell?.btn_Click.TouchUpInside -= BtnClickEvent;
    }
}

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        // 彻底清理所有单元格的事件绑定
        foreach (var cell in tableView.VisibleCells)
        {
            var customCell = cell as CustomCell;
            customCell?.btn_Click.TouchUpInside -= BtnClickEvent;
        }
        tableView.Dispose();
    }
    base.Dispose(disposing);
}

然后你的GetCell方法保持不变,但要确保单元格是自定义的,按钮是成员变量:

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell("CustomCell") as CustomCell;
    if (cell == null)
    {
        cell = new CustomCell(new NSString("CustomCell"));
    }

    // 先移除再添加,确保只有一次绑定
    cell.btn_Click.TouchUpInside -= BtnClickEvent;
    cell.btn_Click.Tag = indexPath.Row;
    cell.btn_Click.TouchUpInside += BtnClickEvent;

    return cell;
}

private void BtnClickEvent(object sender, EventArgs e)
{
    if (sender is UIButton btn)
    {
        int row = btn.Tag;
        // 处理点击逻辑
    }
}

内容的提问来源于stack exchange,提问作者Harshad Pansuriya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:11:30