UICollectionViewのCellをDrag&Dropで動かした方法(Sectionまたぎ有)についてまとめておきます。
イメージとしてはiOSのホーム画面でAppの配置を動かすようなものです。
UITableViewについては割に資料があったのですが、CollectionViewについては「同じように・・・」と端折られてることが多く、色々調べて試しながら進みました。

前提となるCollectionViewは以下で作成したものです
Drag and Dropの実装
delegate設定
class ViewController: UIViewController {
...
override func viewDidLoad() {
...
collectionView.dragDelegate = self
collectionView.dropDelegate = self
}
}
UICollectionViewDragDelegate
の実装
extension ViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.item]
return [dragItem]
}
}
UICollectionViewDropDelegate
の実装
extension ViewController: UICollectionViewDropDelegate {
// Dropしたときの動作
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
if let destinationIndexPath = coordinator.destinationIndexPath {
reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
// Drag中の動作
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
} else {
return UICollectionViewDropProposal(operation: .forbidden)
}
}
private func reorderItems(coordinator: UICollectionViewDropCoordinator,
destinationIndexPath: IndexPath,
collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1,
let item = items.first,
let sourceIndexPath = item.sourceIndexPath,
let localObject = item.dragItem.localObject as? String {
collectionView.performBatchUpdates({
data[sourceIndexPath.section].remove(at: sourceIndexPath.item)
data[destinationIndexPath.section].insert(localObject as String, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
}
}
これで一応動くようにはなりますがSectionがあることによって、空になったSectionにCellを移動できない問題が発生します
※1Bが2Bになってしまっているなど、一部Cellの文字が前後と違いますが間違いですので、無視してください
中身が空になったSectionにCellを移動するための対策
Dragが始まったら空のSectionに仮のCellを追加し、Dragが終わったら仮のCellを取り除くようにします。
表示するデータの変更
enum Model {
case simple(text: String)
case availableToDropAtEnd
}
dataも変えます
private var data = [["0A", "0B", "0C", "0D", "0E"],["1A", "1B"],["2A", "2B", "2C"]]
.map { $0.map { return Model.simple(text: $0) } }
collectionView(_: cellFrotItemAt:)
も変更します
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
switch data[indexPath.section][indexPath.item] {
case Model.simple(text: let text):
cell.label.text = text
cell.backgroundColor = .green
case Model.availableToDropAtEnd:
cell.label.text = "仮"
cell.backgroundColor = .lightGray
}
return cell
}
DragDelegate
に追加実装
extension ViewController: UICollectionViewDragDelegate {
// drag中のアニメーションは記載済
...
// dragSessionが始まるときに、Model.availableToDropAtEndを追加
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
var itemsToInsert = [IndexPath]()
(0 ..< data.count).forEach {
itemsToInsert.append(IndexPath(item: data[$0].count, section: $0))
data[$0].append(.availableToDropAtEnd)
}
collectionView.insertItems(at: itemsToInsert)
}
// dragSessionが終わるときに削除
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
var removeItems = [IndexPath]()
for section in 0 ..< data.count {
for item in 0 ..< data[section].count {
switch data[section][item] {
case .availableToDropAtEnd:
removeItems.append(IndexPath(item: item, section: section))
case .simple:
break
}
}
}
removeItems.forEach { indexPath in
data[indexPath.section].remove(at: indexPath.item)
}
collectionView.deleteItems(at: removeItems)
}
}
これで追加できるようになります。

誤りや、もっと良い実装方法がありましたら教えていただければ幸いです。
全体のコード
参考
もっとも参考にさせていただきました。
最初に試行錯誤を始めたときに見たApple公式
Apple公式を解説していただいている記事
書籍
「iOS 11 Programming」 pp.167-174
ありがとうございました。