2

Update on July 8 2022 - Apple appears to have fixed the two finger scrolling bug, although the interaction is still a bit buggy.


Collection view + compositional layout + diffable data source + drag and drop does not seem to work together. This is on a completely vanilla example modeled after this (which works fine.)

Dragging an item with one finger works until you use a second finger to simultaneously scroll, at which point it crashes 100% of the time. I would love for this to be my problem and not an Apple oversight.

I tried using a flow layout and the bug disappears. Also it persists even if I don't use the list configuration of compositional layout, so that's not it.

Any ideas? Potential workarounds? Is this a known issue?

(The sample code below should run as-is on a blank project with a storyboard containing one view controller pointing to the view controller class.)

import UIKit

struct VideoGame: Hashable {
    let id = UUID()
    let name: String
}

extension VideoGame {
    static var data = [VideoGame(name: "Mass Effect"),
                       VideoGame(name: "Mass Effect 2"),
                       VideoGame(name: "Mass Effect 3"),
                       VideoGame(name: "ME: Andromeda"),
                       VideoGame(name: "ME: Remaster")]
}



class CollectionViewDataSource: UICollectionViewDiffableDataSource<Int, VideoGame> {

    // 1
    override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        guard let fromGame = itemIdentifier(for: sourceIndexPath),
              sourceIndexPath != destinationIndexPath else { return }
        
        var snap = snapshot()
        snap.deleteItems([fromGame])
        
        if let toGame = itemIdentifier(for: destinationIndexPath) {
            let isAfter = destinationIndexPath.row > sourceIndexPath.row
            
            if isAfter {
                snap.insertItems([fromGame], afterItem: toGame)
            } else {
                snap.insertItems([fromGame], beforeItem: toGame)
            }
        } else {
            snap.appendItems([fromGame], toSection: sourceIndexPath.section)
        }
        
        apply(snap, animatingDifferences: false)
    }
}






class DragDropCollectionViewController: UIViewController {
    
    var videogames: [VideoGame] = VideoGame.data
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .insetGrouped)))
    
    lazy var dataSource: CollectionViewDataSource = {
        
        let dataSource = CollectionViewDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, model) -> UICollectionViewListCell in

            return collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: model)

            })

        return dataSource
    }()
    
    let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, VideoGame> { (cell, indexPath, model) in

        var configuration = cell.defaultContentConfiguration()
        configuration.text = model.name
        cell.contentConfiguration = configuration
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        collectionView.dragDelegate = self
        collectionView.dropDelegate = self
        collectionView.dragInteractionEnabled = true
        
        var snapshot = dataSource.snapshot()
        snapshot.appendSections([0])
        snapshot.appendItems(videogames, toSection: 0)
        dataSource.applySnapshotUsingReloadData(snapshot)
    }
}

extension DragDropCollectionViewController: UICollectionViewDragDelegate {
    
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let item = dataSource.itemIdentifier(for: indexPath) else {
            return []
        }
        let itemProvider = NSItemProvider(object: item.id.uuidString as NSString)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = item

        return [dragItem]
    }
}

// 4
extension DragDropCollectionViewController: UICollectionViewDropDelegate {
    
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
    }

    
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        //Not needed
    }
}

JBZic
  • 53
  • 6
  • Either you're right, or we're missing something. Two notes ignoring the crashing: 1) using your code as-is, I cannot re-order the items. While dragging, it *appears* to be working as expected, but dropping anywhere puts the item back where it started (not in the new position). 2) add `print("drop proposal", destinationIndexPath)` to `dropSessionDidUpdate` and slowly drag... it shows the `destinationIndexPath` changing back to the item's origin path regardless of new position. – DonMag Dec 15 '21 at 16:14
  • Oh oops, I actually never tried dropping on this sample. I did get the drop working in my actual project however and am getting the same bug, so I think it's irrelevant. (Although I'll double check to be sure.) Big bummer to hear I might be right and it's an Apple issue. Thanks for the response! – JBZic Dec 15 '21 at 22:26
  • So does this code work as is? I am trying to find out the same thing – devjme Jul 08 '22 at 16:26
  • Apple fixed the two finger scroll bug it appears. It all of a sudden stopped crashing in my project (I had paused on a solution to work on other features.) Testing the code above it also looks like it doesn't crash anymore with two fingers. It's still super buggy, however, and I may resort to a custom implementation of drag and drop in the future. (The code above does not persist the drop by the way - didn't bother figuring out why as it's just a sample I pulled from online and the drop persistence wasn't the focus.) – JBZic Jul 08 '22 at 22:14

1 Answers1

0

If you download the modern Collectionviews project from Apple, there is one that shows compositional layout, diffable datasource and reordering. However this is only for their new list cells, not a reg CollectionView cell.

You can find it here: Modern CollectionViews

devjme
  • 684
  • 6
  • 12
  • Yeah, the reordering API is different from the drag and drop API. Last I looked at it you had to have the reordering handlers visible for it to work which won't work with my UI. Perhaps I'm incorrect and there's a workaround. (You also can't use it to drag and drop between windows or other views.) – JBZic Jul 08 '22 at 22:15
  • I got this working with the regular collection view delegate methods. I just had to clean up the snapshot / reconfigure some items – devjme Jul 13 '22 at 14:30
  • Do you mean the reordering API or the drag and drop API? The reordering API (that only works on lists) doesn't use delegate methods - just the`reorderingHandlers` property on the data source right? – JBZic Jul 15 '22 at 05:28
  • Yes this is what I mean, the reordering handlers – devjme Oct 18 '22 at 14:20