最近项目中需要实现个类似苹果手机桌面上,APP拖拽的效果。拖拽到指定的位置,更新位置并调整布局。记录下实现思路
collectionView基本使用
swift
let layout = UICollectionViewFlowLayout()
collection = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: layout)
collection.backgroundColor = .white
collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "CELL")
collection.dataSource = self
collection.delegate = self
view.addSubview(collection)
// 实现代理
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath)
cell.backgroundColor = .orange
let image = UIImageView(image: UIImage(named: items[indexPath.row])!)
cell.contentView.addSubview(image)
image.snp.makeConstraints { make in
make.edges.equalTo(cell.contentView)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: ((collectionView.frame.width - 32) / 2), height: ((collectionView.frame.width - 32) / 3))
}
实现拖拽效果
collectionView有个叫dragDelegate的protocol
An interface that you implement to interact with a drop operation in a view modified to accept drops.
实现这个protocol的collectionView(_ collectionView: UICollectionView, itemsForBeginning session: any UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
方法显示拖拽效果
swift
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.items[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
实现这个方法之后,collectionView就有了可以拖拽的效果
但拖拽后item并不会在松开拖拽的位置保存下来,还需要实例另一个protocol的代理方法
dropDelegate
官方解释:The delegate object that manages the dropping of items into the collection view.
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal
实现该方法,告诉DropDelegate当我们drag一个cell到一个新位置的时候会发生什么
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)
该方法用于计算drag的cell是否越界、目的地对应的indexPath以及要自己实现的重新排列collectionView的规则
swift
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationIndePath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndePath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndePath = IndexPath(item: row - 1, section: 0)
}
if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndePath, collectionView: collectionView)
}
}
reorderItems方法实现如下
swift
fileprivate func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates({
self.items.remove(at: sourceIndexPath.item)
self.items.insert(item.dragItem.localObject as! String, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: nil)
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
}
调用collectionView.performBatchUpdates和coordinator.drop实现被drag的cell放置在指定的indexPath上
其他补充
有时自己自定义的cell可能会有一些类似圆角、阴影之类的效果,drag的时候,会默认给一个拖拽的背景效果,导致一些不和谐的UI出来,可以实现两个方法来取消这种默认效果
swift
func collectionView(_ collectionView: UICollectionView, dropPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? {
let previewParameters = UIDragPreviewParameters()
// 设置透明背景以移除白色背景
previewParameters.backgroundColor = .clear
previewParameters.shadowPath = UIBezierPath(rect: .zero) // 隐藏阴影
return previewParameters
}
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? {
let previewParameters = UIDragPreviewParameters()
// 设置透明背景以移除白色背景
previewParameters.backgroundColor = .clear
previewParameters.shadowPath = UIBezierPath(rect: .zero) // 隐藏阴影
return previewParameters
}
这两个分别是drop和drag 里相关的方法,我的实践是都要实现一遍
如果要实现一个collectionView中,某些cell不需要drag,一个简单的方式是在func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
中,判断出不需要drag的cell后直接返回个空数组即可。
如果你的collectionView的dataSource是动态变化的,在上面的reorderItems方法中,最好做下边界值的越界判断
swift
if destinationIndexPath.row >= self.data?.count ?? 0 {
return
}
其他可能出现的坑,请随时评论