CollectionViewでSectionがあるときにDrag & Dropで困ること

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505421/2d4939da-2e0c-f78f-0988-66f17e69ccf8.gif" width=200px>

前提となるCollectionViewは以下で作成したものです

https://qiita.com/fumi2mi/items/56cbe24cc20f9ed02f50

## Drag and Dropの実装
delegate設定

```swift
class ViewController: UIViewController {
    ...

    override func viewDidLoad() {
        ...

        collectionView.dragDelegate = self
        collectionView.dropDelegate = self
    }
}
```

`UICollectionViewDragDelegate`の実装

```swift
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`の実装

```swift
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を移動できない問題が発生します
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505421/1a0a0e89-89a6-fe3b-0d59-2b64a16ea3f6.gif" width=200px>
※1Bが2Bになってしまっているなど、一部Cellの文字が前後と違いますが間違いですので、無視してください

## 中身が空になったSectionにCellを移動するための対策
Dragが始まったら空のSectionに仮のCellを追加し、Dragが終わったら仮のCellを取り除くようにします。
表示するデータの変更

```swift
enum Model {
    case simple(text: String)
    case availableToDropAtEnd
}
```
dataも変えます

```swift
    private var data = [["0A", "0B", "0C", "0D", "0E"],["1A", "1B"],["2A", "2B", "2C"]]
        .map { $0.map { return Model.simple(text: $0) } }
```
`collectionView(_: cellFrotItemAt:)`も変更します

```swift
    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`に追加実装

```swift
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)
    }
}
```
これで追加できるようになります。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505421/a8c6150b-f54f-89ef-c8b7-dbd72cd79311.gif" width =200px>

誤りや、もっと良い実装方法がありましたら教えていただければ幸いです。

## 全体のコード
https://github.com/fumi2mi/CollectionViewDragAndDropSample

## 参考
もっとも参考にさせていただきました。

https://stackoverflow.com/questions/39080807/drag-and-reorder-uicollectionview-with-sections/39088025#39088025

最初に試行錯誤を始めたときに見たApple公式

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/supporting_drag_and_drop_in_collection_views

https://developer.apple.com/videos/play/wwdc2017/223/

https://developer.apple.com/videos/play/wwdc2018/225/

Apple公式を解説していただいている記事

https://qiita.com/akatsuki174/items/63606d42fb9b39bb97c9

https://qiita.com/shiz/items/2e9771fe56e39544ff3e

書籍
「iOS 11 Programming」 pp.167-174

https://peaks.cc/books/iOS11

ありがとうございました。