如何让Bar Button Item接收拖放?实现集合视图单元格拖拽删除
确实,UIBarButtonItem不是UIView子类,没法直接添加UIDropInteraction,不过我们可以通过自定义UIView作为它的customView来绕开这个限制,下面是完整的实现方案:
步骤1:创建带拖放交互的垃圾桶自定义View
我们先做一个包含垃圾桶图标且支持拖放的UIView,之后把它作为BarButtonItem的customView:
class TrashBarButtonView: UIView { private let trashImageView: UIImageView = { let imageView = UIImageView(image: UIImage(systemName: "trash")) imageView.tintColor = .systemRed imageView.contentMode = .scaleAspectFit return imageView }() // 用闭包传递删除事件给ViewController var onItemDropped: ((Any) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setupLayout() addDropSupport() } required init?(coder: NSCoder) { super.init(coder: coder) setupLayout() addDropSupport() } private func setupLayout() { addSubview(trashImageView) trashImageView.translatesAutoresizingMaskIntoConstraints = false // 约束设置成符合BarButtonItem的标准尺寸 NSLayoutConstraint.activate([ trashImageView.centerXAnchor.constraint(equalTo: centerXAnchor), trashImageView.centerYAnchor.constraint(equalTo: centerYAnchor), trashImageView.widthAnchor.constraint(equalToConstant: 24), trashImageView.heightAnchor.constraint(equalToConstant: 24), widthAnchor.constraint(equalToConstant: 44), heightAnchor.constraint(equalToConstant: 44) ]) } private func addDropSupport() { let dropInteraction = UIDropInteraction(delegate: self) addInteraction(dropInteraction) } } // 实现拖放代理方法 extension TrashBarButtonView: UIDropInteractionDelegate { func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { // 这里替换成你实际的数据源类型,比如自定义Model return session.canLoadObjects(ofClass: NSString.self) } func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { // 拖拽进入时给图标加高亮反馈 trashImageView.tintColor = session.isActive ? .systemRed.withAlphaComponent(0.7) : .systemRed return UIDropProposal(operation: .move) } func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { // 加载拖拽的数据并通知ViewController删除 session.loadObjects(ofClass: NSString.self) { [weak self] items in guard let self = self, let targetItem = items.first else { return } self.onItemDropped?(targetItem) } } // 拖拽离开或结束时恢复图标颜色 func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) { trashImageView.tintColor = .systemRed } func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) { trashImageView.tintColor = .systemRed } }
步骤2:在ViewController中配置导航栏和CollectionView
接下来把自定义的垃圾桶View设置成导航栏右侧按钮,同时给CollectionView添加拖拽支持:
class YourCollectionVC: UIViewController { private var collectionView: UICollectionView! private var dataSource: [String] = ["Item 1", "Item 2", "Item 3", "Item 4"] // 你的数据源 override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupCollectionView() } private func setupNavigationBar() { let trashView = TrashBarButtonView() // 绑定删除事件的处理逻辑 trashView.onItemDropped = { [weak self] item in guard let self = self, let itemToDelete = item as? String, let index = self.dataSource.firstIndex(of: itemToDelete) else { return } // 更新数据源并刷新CollectionView self.dataSource.remove(at: index) self.collectionView.deleteItems(at: [IndexPath(item: index, section: 0)]) } // 把自定义View包装成BarButtonItem let trashBarBtn = UIBarButtonItem(customView: trashView) navigationItem.rightBarButtonItem = trashBarBtn } private func setupCollectionView() { // 基础布局配置 let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: view.bounds.width - 32, height: 60) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell") collectionView.dataSource = self view.addSubview(collectionView) // 添加拖拽支持 let dragInteraction = UIDragInteraction(delegate: self) collectionView.addInteraction(dragInteraction) } } // CollectionView数据源实现 extension YourCollectionVC: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { dataSource.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) cell.backgroundColor = .systemGray5 // 添加单元格内容 if let label = cell.viewWithTag(100) as? UILabel { label.text = dataSource[indexPath.item] } else { let label = UILabel(frame: cell.bounds.inset(by: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16))) label.tag = 100 label.text = dataSource[indexPath.item] cell.addSubview(label) } return cell } } // CollectionView拖拽代理实现 extension YourCollectionVC: UIDragInteractionDelegate { func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { guard let indexPath = collectionView.indexPathForItem(at: session.location(in: collectionView)), let targetItem = dataSource[safe: indexPath.item] else { return [] } // 将数据包装成NSItemProvider,自定义Model需要遵循NSItemProviderWriting协议 let itemProvider = NSItemProvider(object: targetItem as NSString) let dragItem = UIDragItem(itemProvider: itemProvider) // 设置localObject方便后续快速获取数据 dragItem.localObject = targetItem return [dragItem] } } // 数组安全下标扩展,避免越界崩溃 extension Array { subscript(safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } }
关键注意点
- 自定义Model适配:如果你的数据源是自定义Model,需要让Model遵循
NSItemProviderWriting协议,这样才能在拖放时正确序列化和加载数据。 - 视觉反馈:代码中添加了拖拽时的图标高亮,你还可以扩展更多交互反馈,比如缩放、阴影,提升用户体验。
- 操作类型选择:如果希望拖拽后原单元格保留,可以把
UIDropProposal的operation改成.copy,不过删除场景用.move更合理。
内容的提问来源于stack exchange,提问作者Giovanni Fabbretti
相关产品推荐
相关产品推荐

