如何通过点击UITableViewCell内按钮实现类Cell选中的互斥效果?
嘿,这个场景我太熟悉了——用单例做这种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
相关产品推荐
相关产品推荐

