今日は何を学びますか?
アプリケーションのたくさんのテーブルビューにドラッグ・アンド・ドロップ・サポートを追加します。
テーブルビュー間でアイテムをドラッグ・アンド・ドロップできるようになります。

ストーリー
私はかわいい猫を3匹飼っていて、猫用のおもちゃも3つ持っています(実際にはもっとたくさん持っていますが、この例では3つだけということにしておきましょう)。どの猫がどのおもちゃを持っているか追跡したいと思い、簡単なアプリを作りました。
スタータープロジェクト
このチュートリアルに従っていただくために、UITableView
が2つ以上あるアプリを持っている必要があります。この例では、1つのCollectionView
の中に3つのテーブルビューがあります。
スタータープロジェクトはこちらからダウンロードしてください: https://github.com/mszmagic/DragAndDropNekoApp/tree/StartTemplate
リンク先で最初のバージョンをダウンロードしてください。
完成したプロジェクト
プロジェクトを直接 git clone
したら完成したプログラムが手に入ります。
ドラッグのサポート
ドラッグがサポートされると、システムユーザーが UITableViewCell
を長押しし、それをドラッグして現在のテーブルの表示から外に出せるようになります。
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
デリゲート (Delegate) インプリメンテーションの追加
/*
Drag delegate
*/
extension catCollectionViewCell: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
}
}
ここでは UIDragItem
を提供する必要があります。UIDragItem
には UITableViewCell
間で移動するデータが必要です。この例では、移動したいデータはオモチャの名前になります。
まずオモチャの名前を入手し:
let toyName = toys[indexPath.row]
次にこれを Data
オブジェクト内にエンコード
guard let toyNameData = toyName.data(using: .utf8) else { return [] }
アイテムプロバイダーを生成し、toyNameData
をこのプロバイダーにセット。
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
completion(toyNameData, nil)
return nil
}
ここで、その結果を戻す
let item = UIDragItem(itemProvider: provider)
return [item]
ただし、最初のネコの名前が必要になるので、これを session.localContext
に保存
session.localContext = catName
これで1つのテーブル表示のセルをドラッグしてテーブル表示から外に出せるようになったはずです。そして、そのテーブル表示のセルを別のテーブル表示にドロップできるはずです。
でも、ここまでは送り手側の作業を完了しただけです。まだ、これから受け手側のコーディング作業が少し残っています。
ドロップ (Drop) サポートを追加する
ドロップサポートを使うと、別のオブジェクトを現在のビューにドロップできます。ドロップサポートを追加するには、まずテーブルビューにデリゲートを割り当てます。
tableView.dropDelegate = self
次に、delegate
を実装します:
extension catCollectionViewCell: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}
}
coordinator.session
から2つの関数が必要になります。 (https://developer.apple.com/documentation/uikit/uidropsession)
hasItemsConforming(to:)
この関数を使うと、ビューにドロップしたデータのタイプを確認することができます。この記事の1つ前の節で次のコード行を使ったことを思い出してください:
provider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
データのタイプは kUTTypePlainText as String
です。次のコードを実行して確認できます:
if coordinator.session.hasItemsConforming(toTypeIdentifiers: [kUTTypePlainText as String]) { }
loadObjects(ofClass:completion:)
この関数を使うと、coordinator
内にあるデータオブジェクトを抽出できます。
coordinator.session.loadObjects(ofClass: NSString.self) { (fetchedItems) in
//TODO
}
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匹目の猫の名前
次のコード行を書いたことを思い出してください:
session.localContext = catName
猫の名前は session.localContext
に保存してあるので、このコードを使って取得できます:
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) を使用してこの情報を伝達できます。
protocol dragAndDropActionDelegate: AnyObject {
func moveToy(toyName: String, fromCat: String, toCat: String)
}
そして、そこでアクションを実行できるよう、このデリゲート (delegate) を ViewController
内にインプリメントします。
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) 表示に変数を登録。
//catCollectionViewCell.swift
weak var delegate: dragAndDropActionDelegate?
delegate 変数をセット:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
cell.delegate = self
...
}
そして、ドロップのアクションが実行されたときにデリゲートファンクションをコール。
self.delegate?.moveToy(toyName: toyName, fromCat: originalCatName, toCat: self.catName)