2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【UIKit】TableViewのドラッグ&ドロップをCollectionViewで実装する方法

Posted at

はじめに

TableViewで実装していた画面をモダンなCollection Viewに書き換えていく際に、ドラッグ&ドロップで行うセルの順番を入れ替える方法を探したところUITableViewDelegatetableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath:)の処理にあたるものが、UICollectionViewDelegateにはなかったので困りました。

※モダンなCollection Viewに書き換えた場合のUIイメージ(サンプルコードより)

対応方法をまとめていきます。

環境

Xcode 13.2.1
Swift 5.5.2

整理

UITableViewDelegatetableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath:)スクリーンショット 2022-03-06 14.18.58.pngiOS2からあるもののようで、実装が簡単なので使用していました。

UICollectionViewDelegateには同じようなものがないので、iOS11から使えるようになったUICollectionViewDragDelegateスクリーンショット 2022-03-06 14.49.27.pngUICollectionViewDropDelegateスクリーンショット 2022-03-06 14.49.40.pngを使うのがよさそうです。

iOS11以前では、スクリーンショット 2022-03-06 14.46.23.pngこのあたりを使って実現できたりするようです。

TableVIewにもUITableViewDragDelegateUITableViewDropDelegateがあり、既存のTableViewのドラッグ&ドロップの入れ替えをこれで実装している場合は、苦労がなさそうです)

実装方法

コレクションビューのドラッグ&ドロップのやり方が丁寧に書いてありました💡

ドキュメント:Supporting Drag and Drop in Collection Views

ドラッグ時の処理

UICollectionViewDragDelegateの必須メソッドで処理します。

func collectionView(_ collectionView: UICollectionView, 
  itemsForBeginning session: UIDragSession, 
                 at indexPath: IndexPath) -> [UIDragItem]

コレクションビューから項目をドラッグできるようにするには、このメソッドを実装する必要がある。実装では、指定されたindexPathにあるアイテムのために1つまたは複数のUIDragItemオブジェクトを作成する。通常、1つのドラッグアイテムのみを返しますが、条件によって対応が必要。

さらに他のUICollectionViewDragDelegateメソッドを使用することでドラッグ時の挙動をカスタマイズできる

ドラッグ中の処理

optional func collectionView(_ collectionView: UICollectionView, 
        dropSessionDidUpdate session: UIDropSession, 
    withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal

ドラッグしている間、コレクションビューはこのメソッドを繰り返し呼び出し、指定した位置でドロップが発生した場合にどのような処理を行うかを決定する。
(繰り返し呼び出されるので重い処理はしない方がよい)

ここではUICollectionViewDropProposalを返す必要がありますが、2つのパラメータoperation: UIDropOperationintent: UICollectionViewDropProposal.Intentがあり、選択肢は以下の通り

operation: UIDropOperation
  • copy
    アイテムをコピー
  • move
    アイテムを移動
  • forbidden
    移動またはコピー操作は実行するが、ドロップアクティビティは行わない
  • cancel
    ドラッグをキャンセル
intent: UICollectionViewDropProposal.Intent
  • insertAtDestinationIndexPath
    指定したインデックスパスに挿入
  • insertIntoDestinationIndexPath
    指定されたアイテムの中に入れ込む
  • unspecified
    指定なし

ドロップ時の処理

func collectionView(_ collectionView: UICollectionView, 
    performDropWith coordinator: UICollectionViewDropCoordinator)

このメソッドを使用して、ドロップされたコンテンツを受け取り、コレクションビューに統合します。実装では、コーディネータオブジェクトのitemsから各UIDragItemを取得する。コレクションビューのデータソースにデータを組み込み、必要なアイテムを挿入してコレクションビュー自体を更新。アイテムを組み込む際、コーディネータオブジェクトのメソッドを使い、ドラッグアイテムのプレビューからコレクションビューの対応するアイテムへの遷移をアニメーションで表現する。すぐに取り込む項目については、drop(_:to:) またはdrop(_:toItemAt:)メソッドを使用してアニメーションを実行することができる。

実装

ベースのコードはこの記事書きながらModern Collection Viewで実装したテーブルビュー(見た目)です。

まずは、コレクションビューにdragDelegatedropDelegateを追加
その他、collectionView.isSpringLoaded = trueにすることでドラッグ中にフォルダセルの中身が開くようにします(デフォルトはfalseなので)

LessonDataViewController
extension LessonDataViewController: UICollectionViewDelegate {
    func configureCollectionView() {
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout())
        view.addSubview(collectionView)
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .systemGroupedBackground
        self.modernCollectionView = collectionView
        collectionView.delegate = self

        collectionView.dragDelegate = self   👈 追加
        collectionView.dropDelegate = self   👈 追加
        collectionView.isSpringLoaded = true 👈 追加      
    }

    func generateLayout() -> UICollectionViewLayout {
        let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
        let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        return layout
    }
}

👇 追加
extension LessonDataViewController: UICollectionViewDragDelegate, UICollectionViewDropDelegate {
}

それでは、記事のテーマであるドラッグ&ドロップの処理を実装します。

ドラッグ時

dataSource.itemIdentifier(for: indexPath)でドラッグしたセルの情報を取得、dragItem.localObjectに入れて返す処理を加えます。

LessonDataViewController
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let lessonItem = self.dataSource.itemIdentifier(for: indexPath) else { return [] }
        let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = lessonItem
        return [dragItem]
    }
ドラッグ中

ドラッグしたものがグループならinsertAt、アイテムならドロップ先を見てグループorアイテムでinsertIntoorinsertAtを分けるようにしました(やり方があっているのかは分からないですが、それっぽくは動いています)
ちなみにこれdragItem.isGroupは別でデータを見て結果を返す処理を書いています。

LessonDataViewController
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if collectionView.hasActiveDrag {
            if let destinationIndex = destinationIndexPath, let destinationItem = dataSource.itemIdentifier(for: destinationIndex) {
                if let dragItem = session.items.first?.localObject as? LessonItem, dragItem.isGroup {
                    // ドラッグしたアイテムがグループ
                    return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
                } else {
                    // ドラッグしたアイテムがアイテム
                    return UICollectionViewDropProposal(operation: .move, intent: destinationItem.isGroup ? .insertIntoDestinationIndexPath : .insertAtDestinationIndexPath)
                }
            }
        }
        return UICollectionViewDropProposal(operation: .forbidden)
    }
ドロップ時

ここではinsertIntoorinsertAtを判定してからデータ更新の処理を加えます。
insertAtの場合は、coordinator.drop(dragItem, toItemAt: destinationIndexPath)を加えることでアニメーションが効いたので必要そうです。

LessonDataViewController
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        if let destinationIndexPath = coordinator.destinationIndexPath, 
            let destinationItem = dataSource.itemIdentifier(for: destinationIndexPath), 
            let sourceIndexPath = coordinator.items.first?.sourceIndexPath, 
            let dragItem = coordinator.items.first?.dragItem {
            
            guard let dragLessonItem = dragItem.localObject as? LessonItem else { return }
            if coordinator.proposal.intent == .insertIntoDestinationIndexPath {
                // insertIntoの場合
                // ここにデータの処理ロジックを入れる 👈
            } else {
                // insertAtの場合
                coordinator.drop(dragItem, toItemAt: destinationIndexPath)
                // ここにデータの処理ロジックを入れる 👈
            }
        } else {
            assertionFailure("ドラッグ&ドロップに失敗しました。")
        }
    }

おわりに

ざっくりとしたまとめですが、希望の動きになりました。もっと良い書き方がありそうな気もするので、引き続きドキュメントやサンプルコード見ていきたいと思います。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?