LoginSignup
2
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-16

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

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

2
2
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
2