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

如何通过点击UITableViewCell内按钮实现类Cell选中的互斥效果?

解决UITableViewCell内MenuView的互斥显示问题(替代单例方案)

嘿,这个场景我太熟悉了——用单例做这种Cell内的弹窗确实是典型的滥用,不仅会导致状态管理混乱,还会因为单例的生命周期和Cell不匹配出现各种奇怪的问题。咱们换个更符合iOS MVC设计的思路来实现这个同一时间只显示一个Cell的MenuView的效果,逻辑和UITableView的单选选中完全一致,核心就是把控制逻辑从Cell里抽出来,交给TableView的控制器来统一管理。

步骤1:重构ProductsOperationMenu,去掉单例

先把MenuView改成普通的UIView子类,只负责自身的UI初始化和内部交互,不处理任何外部状态:

@implementation ProductsOperationMenu
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self setup]; // 这里是你的UI初始化代码,比如添加按钮、设置样式等
    }
    return self;
}
// 其他内部方法...
@end

步骤2:改造Cell,让它只负责展示和传递事件

Cell不应该自己管理Menu的显示逻辑,而是通过代理(或者Block)把点击事件通知给控制器,同时提供一个方法让控制器控制Menu的显示/隐藏:

ZBMyProductsCell.h

@class ZBMyProductsCell;
@protocol ZBMyProductsCellDelegate <NSObject>
/// 点击Cell内的操作按钮时调用
- (void)cellDidTapOperationButton:(ZBMyProductsCell *)cell;
@end

@interface ZBMyProductsCell : UITableViewCell
@property (nonatomic, weak) id<ZBMyProductsCellDelegate> delegate;
/// 控制Menu的显示状态
- (void)showOperationMenu:(BOOL)show;
@end

ZBMyProductsCell.m

@implementation ZBMyProductsCell
// 懒加载MenuView,避免重复创建,同时保证每个Cell有自己的实例
- (ProductsOperationMenu *)operationMenu {
    if (!_operationMenu) {
        _operationMenu = [[ProductsOperationMenu alloc] initWithFrame:CGRectZero];
        _operationMenu.hidden = YES;
        [self.contentView addSubview:_operationMenu];
        // 布局MenuView,这里用Masonry约束
        [_operationMenu mas_makeConstraints:^(MASConstraintMaker *make) {
            make.width.mas_equalTo(205);
            make.height.mas_equalTo(60);
            make.bottom.mas_equalTo(self.operationButton).offset(0);
            make.right.mas_equalTo(self.operationButton.mas_left).offset(-10);
        }];
    }
    return _operationMenu;
}

- (void)awakeFromNib {
    [super awakeFromNib];
    // 其他初始化代码...
}

- (IBAction)operationButtonClick:(UIButton *)sender {
    // 点击按钮时,通过代理通知控制器
    if ([self.delegate respondsToSelector:@selector(cellDidTapOperationButton:)]) {
        [self.delegate cellDidTapOperationButton:self];
    }
}

- (void)showOperationMenu:(BOOL)show {
    self.operationMenu.hidden = !show;
}

// 处理Cell复用的情况,避免滚动时残留状态
- (void)prepareForReuse {
    [super prepareForReuse];
    self.operationMenu.hidden = YES;
}
@end

步骤3:在TableView控制器里统一管理Menu的显示状态

控制器作为数据源和代理,负责跟踪当前显示Menu的Cell索引,处理互斥逻辑:

@interface YourProductsTableViewController () <ZBMyProductsCellDelegate, UITableViewDataSource>
/// 当前显示Menu的Cell行索引,初始值-1表示没有Cell显示Menu
@property (nonatomic, assign) NSInteger currentMenuShowingIndex;
@end

@implementation YourProductsTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.currentMenuShowingIndex = -1;
    // 注册Cell、设置TableView属性等初始化代码
    [self.tableView registerNib:[UINib nibWithNibName:@"ZBMyProductsCell" bundle:nil] forCellReuseIdentifier:@"ZBMyProductsCell"];
}

#pragma mark - UITableView DataSource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ZBMyProductsCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ZBMyProductsCell" forIndexPath:indexPath];
    cell.delegate = self;
    // 根据当前索引设置Menu的显示状态,解决Cell复用的问题
    [cell showOperationMenu:(indexPath.row == self.currentMenuShowingIndex)];
    // 其他Cell数据填充代码...
    return cell;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.products.count; // 你的数据源数量
}

#pragma mark - ZBMyProductsCellDelegate
- (void)cellDidTapOperationButton:(ZBMyProductsCell *)cell {
    NSIndexPath *tapIndexPath = [self.tableView indexPathForCell:cell];
    if (!tapIndexPath) return;

    if (tapIndexPath.row == self.currentMenuShowingIndex) {
        // 点击的是当前显示Menu的Cell,隐藏Menu
        self.currentMenuShowingIndex = -1;
    } else {
        // 先隐藏之前显示Menu的Cell
        if (self.currentMenuShowingIndex != -1) {
            NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:self.currentMenuShowingIndex inSection:0];
            ZBMyProductsCell *previousCell = (ZBMyProductsCell *)[self.tableView cellForRowAtIndexPath:previousIndexPath];
            if (previousCell) {
                [previousCell showOperationMenu:NO];
            }
        }
        // 设置当前显示Menu的Cell索引
        self.currentMenuShowingIndex = tapIndexPath.row;
    }
    // 刷新当前点击的Cell,更新Menu状态
    [self.tableView reloadRowsAtIndexPaths:@[tapIndexPath] withRowAnimation:UITableViewRowAnimationNone];
}
@end

为什么这个方案更好?

  • 符合MVC设计:Cell只负责展示和传递事件,控制器负责业务逻辑和状态管理,代码职责更清晰
  • 解决Cell复用问题:在cellForRowAtIndexPath里根据索引设置Menu状态,同时在prepareForReuse里重置状态,避免滚动时出现错误的显示
  • 避免单例滥用:每个Cell有自己的Menu实例,生命周期和Cell绑定,不会出现单例带来的内存泄漏或状态混乱问题
  • 扩展性更强:如果后续需要支持多选,只需要把currentMenuShowingIndex改成数组存储选中索引即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:53:01