如何正确实现每个Cell带删除按钮的CollectionView?
CollectionView Cell 添加删除按钮的最优方案分析
先直接给结论:方案1(自定义Cell)的思路更靠谱,但可以通过优化避免“每个Cell类型都要子类化”的问题;方案2不推荐,因为它违背了MVC的职责划分,且复用逻辑容易出bug。
对现有两种方案的分析
方案1(自定义Cell子类):
缺点确实是需要为每种现有Cell创建子类,但优点是UI封装性强,复用逻辑清晰,不会把UI修改逻辑混入数据源方法。这个缺点其实是可以解决的,不用硬给每个Cell做子类。方案2(在cellForItemAt中添加按钮):
完全不推荐。数据源方法的核心职责是提供数据、配置Cell的数据内容,把UI修改(加按钮)放在这里会让代码职责混乱,后期维护难度大。另外,Cell复用时用标签判断按钮是否存在的方式风险很高,很容易和第三方库的标签冲突,导致按钮重复添加或者找不到的问题。
更优的实现方式
推荐两种更实用的思路,不用修改现有Cell结构,也能优雅地添加删除按钮:
1. 通用装饰Cell(推荐)
创建一个专门的DeleteableCollectionViewCell,作为“容器”来包裹原有Cell:
- 这个容器Cell内部包含一个
contentContainerView(用来承载原有Cell的内容)和一个删除按钮; - 在
cellForItemAt方法中,先获取这个容器Cell,再获取原本需要展示的目标Cell,把目标Cell添加到contentContainerView中,设置约束让目标Cell充满容器; - 删除按钮的点击逻辑统一在容器Cell中处理,通过代理或者闭包通知外部删除事件。
这种方式的好处是:原有Cell完全不用修改,不管有多少种Cell类型,都可以用同一个容器Cell来添加删除按钮,彻底解决“每个Cell都要子类化”的问题,同时保持UI逻辑的封装性。
2. UICollectionViewCell 扩展(轻量方案)
通过给UICollectionViewCell写扩展,用关联对象存储删除按钮,避免重复创建:
import UIKit private var deleteButtonAssociatedKey = "deleteButtonAssociatedKey" private var deleteActionKey = "deleteActionKey" extension UICollectionViewCell { private var deleteButton: UIButton? { get { return objc_getAssociatedObject(self, &deleteButtonAssociatedKey) as? UIButton } set { objc_setAssociatedObject(self, &deleteButtonAssociatedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } private var deleteAction: (() -> Void)? { get { return objc_getAssociatedObject(self, &deleteActionKey) as? () -> Void } set { objc_setAssociatedObject(self, &deleteActionKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) } } func configDeleteButton(withAction action: @escaping () -> Void) { guard deleteButton == nil else { // 复用的时候更新点击事件 deleteButton?.actions(forTarget: self, forControlEvent: .touchUpInside)?.forEach { removeTarget(self, action: Selector($0), for: .touchUpInside) } deleteButton?.addTarget(self, action: #selector(deleteButtonTapped(_:)), for: .touchUpInside) self.deleteAction = action return } let button = UIButton(type: .system) button.setTitle("删除", for: .normal) button.backgroundColor = .red button.setTitleColor(.white, for: .normal) button.layer.cornerRadius = 4 // 固定按钮位置,比如右上角 button.frame = CGRect(x: bounds.width - 65, y: 8, width: 60, height: 30) button.addTarget(self, action: #selector(deleteButtonTapped(_:)), for: .touchUpInside) addSubview(button) bringSubviewToFront(button) self.deleteButton = button self.deleteAction = action } @objc private func deleteButtonTapped(_ sender: UIButton) { deleteAction?() } }
使用时,在cellForItemAt中只需要调用:
cell.configDeleteButton { [weak self] in // 处理删除逻辑,比如删除数据源对应的数据,刷新CollectionView guard let self = self else { return } self.dataSource.remove(at: indexPath.item) self.collectionView.deleteItems(at: [indexPath]) }
这种方式不用创建新的Cell子类,也不会污染数据源方法的职责,复用的时候也能正确处理按钮的状态和事件。
总结
- 如果项目中Cell类型较多,优先用通用装饰Cell,结构更清晰,扩展性更强;
- 如果只是简单给少数Cell加删除按钮,用Cell扩展的方式更轻量;
- 绝对不要用方案2,后期维护会有一堆坑。
内容的提问来源于stack exchange,提问作者Andy
相关产品推荐
相关产品推荐

