UITableView のドラッグ・アンド・ドロップ  (Drag & Drop)・サポートの追加

## 今日は何を学びますか？

アプリケーションのたくさんのテーブルビューにドラッグ・アンド・ドロップ・サポートを追加します。

テーブルビュー間でアイテムをドラッグ・アンド・ドロップできるようになります。

<img width="320" alt="Screen Shot 2020-05-27 at 2.26.54 PM.png" src="https://mszmagic.s3-ap-northeast-1.amazonaws.com/qitta_article_dragDrop_20200528.gif">

## ストーリー

私はかわいい猫を3匹飼っていて、猫用のおもちゃも3つ持っています（実際にはもっとたくさん持っていますが、この例では3つだけということにしておきましょう）。どの猫がどのおもちゃを持っているか追跡したいと思い、簡単なアプリを作りました。

### スタータープロジェクト

このチュートリアルに従っていただくために、`UITableView` が2つ以上あるアプリを持っている必要があります。この例では、1つの`CollectionView` の中に3つのテーブルビューがあります。

スタータープロジェクトはこちらからダウンロードしてください: https://github.com/mszmagic/DragAndDropNekoApp/tree/StartTemplate

リンク先で最初のバージョンをダウンロードしてください。

### 完成したプロジェクト

プロジェクトを直接 `git clone` したら完成したプログラムが手に入ります。

https://github.com/mszmagic/DragAndDropNekoApp

## :sparkles:

### ドラッグのサポート

ドラッグがサポートされると、システムユーザーが `UITableViewCell` を長押しし、それをドラッグして現在のテーブルの表示から外に出せるようになります。

```swift
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
```

デリゲート (Delegate) インプリメンテーションの追加

```swift
/*
 Drag delegate
 */
extension catCollectionViewCell: UITableViewDragDelegate {
    
    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        
    }
    
}
```

ここでは `UIDragItem` を提供する必要があります。`UIDragItem` には `UITableViewCell` 間で移動するデータが必要です。この例では、移動したいデータはオモチャの名前になります。

まずオモチャの名前を入手し:

```swift
let toyName = toys[indexPath.row]
```

次にこれを `Data` オブジェクト内にエンコード

```swift
guard let toyNameData = toyName.data(using: .utf8) else { return [] }
```

アイテムプロバイダーを生成し、`toyNameData` をこのプロバイダーにセット。

```swift
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
    completion(toyNameData, nil)
    return nil
}
```

ここで、その結果を戻す

```swift
let item = UIDragItem(itemProvider: provider)
return [item]
```

ただし、最初のネコの名前が必要になるので、これを `session.localContext` に保存

```swift
session.localContext = catName
```
これで1つのテーブル表示のセルをドラッグしてテーブル表示から外に出せるようになったはずです。そして、そのテーブル表示のセルを別のテーブル表示にドロップできるはずです。　

https://github.com/mszmagic/DragAndDropNekoApp/blob/master/NekoApp/Views/catCollectionViewCell.swift#L50...L72

でも、ここまでは送り手側の作業を完了しただけです。まだ、これから受け手側のコーディング作業が少し残っています。

### ドロップ (Drop) サポートを追加する

ドロップサポートを使うと、別のオブジェクトを現在のビューにドロップできます。ドロップサポートを追加するには、まずテーブルビューにデリゲートを割り当てます。

```swift
tableView.dropDelegate = self
```

次に、`delegate` を実装します：

```swift
extension catCollectionViewCell: UITableViewDropDelegate {
    
    func tableView(_ tableView: UITableView, performDropWith     coordinator: UITableViewDropCoordinator) {
        
    }
    
}
```

`coordinator.session` から2つの関数が必要になります。 (https://developer.apple.com/documentation/uikit/uidropsession)

#### hasItemsConforming(to:)

https://developer.apple.com/documentation/swiftui/dropinfo/3284316-hasitemsconforming

この関数を使うと、ビューにドロップしたデータのタイプを確認することができます。この記事の1つ前の節で次のコード行を使ったことを思い出してください：

```swift
provider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
```

データのタイプは `kUTTypePlainText as String` です。次のコードを実行して確認できます：

```swift
if coordinator.session.hasItemsConforming(toTypeIdentifiers: [kUTTypePlainText as String]) { }
```

#### loadObjects(ofClass:completion:)

この関数を使うと、`coordinator` 内にあるデータオブジェクトを抽出できます。

```swift
coordinator.session.loadObjects(ofClass: NSString.self) { (fetchedItems) in
    //TODO
}
```

```swift
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    //データのタイプは `kUTTypePlainText as String` です。次のコードを実行して確認できます
    if coordinator.session.hasItemsConforming(toTypeIdentifiers: [kUTTypePlainText as String]) {
        //この関数を使うと、`coordinator` 内にあるデータオブジェクトを抽出できます。
        coordinator.session.loadObjects(ofClass: NSString.self) { (fetchedItems) in
            guard let toyName = fetchedItems.first as? String else { return }
            //TODO
        }
    }
}
```

そして、おもちゃの名前が分かれば、そのおもちゃを1匹目の猫から削除して、2匹目の猫に追加できます。

ここでは、おもちゃの名前、1匹目の猫の名前、2匹目の猫の名前が必要です：

##### おもちゃの名前：

名前の情報は変数toyNameから取得できます。

##### 1匹目の猫の名前

次のコード行を書いたことを思い出してください：

```swift
session.localContext = catName
```

猫の名前は `session.localContext` に保存してあるので、このコードを使って取得できます：

```swift
if let originalCatName = coordinator.session.localDragSession?.localContext as? String {
    //1匹目の猫の名前
}
```

##### 2匹目の猫の名前

`self.catName` を呼び出すことで取得できます。

#### ドラッグ機能の追加に関するコードはここで見ることができます：https://github.com/mszmagic/DragAndDropNekoApp/blob/master/NekoApp/Views/catCollectionViewCell.swift#L50...L72

#### ドロップ機能の追加に関するコードはここで見ることができます：https://github.com/mszmagic/DragAndDropNekoApp/blob/master/NekoApp/Views/catCollectionViewCell.swift#L74...L93

### データを更新する

これで、次のプログラム値が得られました。
- おもちゃの名前
- 最初の猫の名前
- 2番目の猫の名前

次のことが必要です。
- 最初の猫からおもちゃを外す
- 2番目の猫におもちゃを追加する
- テーブルビューをリロードする

これら3つのタスクは `ViewController` で完了するため、プログラム・デリゲート (Delegate) を使用してこの情報を伝達できます。

```swift
protocol dragAndDropActionDelegate: AnyObject {
    func moveToy(toyName: String, fromCat: String, toCat: String)
}
```

そして、そこでアクションを実行できるよう、このデリゲート (delegate) を `ViewController` 内にインプリメントします。

```swift
extension ViewController: dragAndDropActionDelegate {
    
    func moveToy(toyName: String, fromCat: String, toCat: String) {
        //最初の猫からおもちゃを外す
        var fromCatToys = catToys[fromCat] ?? []
        fromCatToys.removeAll { (toyNameInArray) -> Bool in
            return toyNameInArray == toyName
        }
        catToys[fromCat] = fromCatToys
        //2番目の猫におもちゃを追加する
        var toCatToys = catToys[toCat] ?? []
        toCatToys.append(toyName)
        catToys[toCat] = toCatToys
        //テーブルビューをリロードする
        collectionView.reloadData()
    }
    
}
```

テーブル (catCollectionViewCell) 表示に変数を登録。

```swift
//catCollectionViewCell.swift
weak var delegate: dragAndDropActionDelegate?
```

delegate 変数をセット:

```swift
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    ...
    cell.delegate = self
    ...
}
```

そして、ドロップのアクションが実行されたときにデリゲートファンクションをコール。

```swift
self.delegate?.moveToy(toyName: toyName, fromCat: originalCatName, toCat: self.catName)
```

#### データ配列のアップデートに関連するコードは以下の通り https://github.com/mszmagic/DragAndDropNekoApp/commit/97fb908ce09b031b07c6bbc835fe43a59a9780c2

### 完成したプロジェクト: https://github.com/mszmagic/DragAndDropNekoApp

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">