概覽
UIKit 中的 UICollectionView 視圖是我們顯示多列集合數據的不二選擇,而豐富多彩的交互操作更是我們選擇 UICollectionView 視圖的另一個重要原因。
如上圖所示:我們實現了在 UICollectionView 中拖放交換任意兩個 Cell 子視圖的功能,這是怎么做到的呢?
在本篇博文中,您將學到如下內容:
- 概覽
- 1. UICollectionView 拖放交互基本思路
- 2. 構建 storyboard
- 3. 準備數據源
- 4. 遵守拖放代理
- 5. 更快的響應交換操作
- 總結
其實,完成這樣一種交換遠比小伙伴們想象的要簡單的多!
所以,還等什么呢?Let‘s find out!!!😉
1. UICollectionView 拖放交互基本思路
在 iOS(iPadOS/MacOS) 中,廣義的拖放操作涉及到跨越不同 App 間的范疇:比如,我們常常希望將微信中的圖片直接拖動到 QQ 的聊天界面中去。
不過,這里我們只想在 App 內部進行拖動,所以完成起來就要簡單許多。我們的數據元素不需要滿足 NSItemProvider 或 NSSecureCoding 等一些苛刻限制,只是單純的數據即可。
一般來說,為了完成拖放,我們需要在對應視圖上實現 UIDragInteractionDelegate 和 UIDropInteractionDelegate 協議指定的方法。
而對于 UICollectionView 視圖,其子 Cell 間的拖動我們還有更簡單的方式:遵守 UICollectionViewDragDelegate 和 UICollectionViewDropDelegate 協議即可。
iOS(iPadOS/MacOS) 系統中關于拖放(Drag & Drop)更詳細全面的介紹,蘋果官網無疑是一個很好的選擇:
- ? Drag and drop
2. 構建 storyboard
首先,新建一個 UIKit 項目,打開 Main 故事板(Main.storyboard)為視圖控制器添加一個 UICollectionView 子視圖:
如上圖所示,我們隨后又在 UICollectionView 中添加了一個靜態的 UICollectionViewCell 控件:
然后,調整 UICollectionViewCell 背景以及 Label 字體大小和顏色到滿意為止:
最后,妥善設置好 UICollectionViewCell 的 ID:
并將 UICollectionView 關聯到視圖控制器的 collectionView 屬性上:
3. 準備數據源
現在界面布局已準備就緒,我們接下來需要創建數據模型:
struct Item {var id = UUID()var title: Stringstatic var preview: [Item] {[Item(title: "Apple"), Item(title: "Banana"),Item(title: "Cherry"), Item(title: "Date"),Item(title: "Dragon"), Item(title: "Sheep"),Item(title: "V-Malicious"), Item(title: "X-Code"),Item(title: "GreatWall"), Item(title: "TaiTan"),Item(title: "Milk"), Item(title: "🥸"),]}
}
是滴,我們只需要一個簡單的結構類型就可以了!
然后,讓我們的 ViewController 遵守 UICollectionViewDataSource 協議,并實現相關方法:
class ViewController: UIViewController, UICollectionViewDataSource {@IBOutlet weak var collectionView: UICollectionView!var items = Item.previewfunc collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {items.count}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cellcell.layer.cornerRadius = 15.0cell.label.text = items[indexPath.row].titlereturn cell}override func viewDidLoad() {super.viewDidLoad()collectionView.dataSource = self}
}
注意,我們并沒有讓 ViewController 遵守 UICollectionViewDelegate 協議,因為它和這里的拖動操作基本上沒有半毛線關系。
4. 遵守拖放代理
上面說過,為了讓 UICollectionView 中的 Cell 子視圖支持拖放,我們需要先讓視圖控制器遵守 UICollectionViewDragDelegate 和 UICollectionViewDropDelegate 協議:
extension ViewController: UICollectionViewDragDelegate {func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {// 保存源 Item 索引srcIndex = indexPathlet itemProvider = NSItemProvider(object: "Item" as NSString)return [UIDragItem(itemProvider: itemProvider)]}
}extension ViewController: UICollectionViewDropDelegate {func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {// 保存目的 Item 索引self.destIndex = destinationIndexPathreturn .init(operation: .move)}func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) { guard let srcIndex, let destIndex else { return }swapItems(from: srcIndex, to: destIndex)self.srcIndex = nilself.destIndex = nil}
}
對于上面的代碼,需要說明的是:
- collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) 在拖動開始時被調用,我們可以趁機保存源 Item 的信息;
- collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) 在拖動中被多次調用,其中傳入的 destinationIndexPath 參數表示目的 Cell 的索引(如果下方有的話),我們可以借此保存目的 Item 的信息;
- collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) 在結束放置時被調用,我們此時可以實現交換對應 Cell 的操作;
隨后,我們還需要在 ViewController 中添加所需的屬性和方法:
var srcIndex: IndexPath?
var destIndex: IndexPath?private func swapItems(from srcIndex: IndexPath, to destIndex: IndexPath) {items.swapAt(srcIndex.row, destIndex.row)// 將源和目的 Cell 的移動放到批處理中以產生流暢的動畫:collectionView.performBatchUpdates {collectionView.moveItem(at: srcIndex, to: destIndex)collectionView.moveItem(at: destIndex, to: srcIndex)}
}
最后,在視圖控制器加載時為其綁定對應的拖放代理,并開啟拖動交互:
override func viewDidLoad() {super.viewDidLoad()collectionView.dataSource = selfcollectionView.dropDelegate = selfcollectionView.dragDelegate = selfcollectionView.dragInteractionEnabled = true
}
現在,運行 App 看一下效果:
不過,小伙伴們或許發現了,拖動放置后 Cell 交換會有略微延時,這是怎么回事呢?
5. 更快的響應交換操作
上面 Cell 拖動交換會慢一拍的原因為:我們是在 collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) 方法中執行 swapItems() 來完成交換的。
而系統對該方法的回調觸發是比較“謹慎”的,它會在判斷用戶徹底抬起手指后才能得到運行機會。
如果希望用戶在目標 Cell 上抬起手指時能夠立即完成交換行為,我們可以將 swapItems() 方法放到 UICollectionViewDragDelegate 協議中的 collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) 的回調中去執行,因為該方法識別觸發的速度要快得多:
extension ViewController: UICollectionViewDragDelegate {func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {guard let srcIndex, let destIndex else { return }swapItems(from: srcIndex, to: destIndex)self.srcIndex = nilself.destIndex = nil}
}
再次運行 App 看一下:
現在,我們 UICollectionView 中的拖放交換操作的速度又上了一個新臺階,還不快給自己一個大大的贊嗎?棒棒噠!💯
總結
在本篇博文中,我們討論了 UIKit 中 UICollectionView 視圖拖放操作的基本原理,并用最簡單的代碼實現了 UICollectionView 視圖中 Cell 的交換功能。
感謝觀賞,再會!😎