Problem with drag/drop chapter 3

So after i finished chapter 3 and started doing testing my method was something like this

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
    
    
    if coordinator.session.localDragSession != nil {
      collectionView.performBatchUpdates({
        for item in coordinator.items {
          guard let sourceIndex = item.sourceIndexPath else {
            return
          }
          
          self.entry?.images.remove(at: sourceIndex.item)
          self.collectionView.deleteItems(at: [sourceIndex])
        }
      })
    }
      

    
    let destinationIndex = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)
    coordinator.session.loadObjects(ofClass: UIImage.self) {  [weak self] imageItems in
      
      guard let self = self else { return }
      let images = imageItems as! [UIImage]
      
      self.entry?.images.insert(contentsOf: images, at: destinationIndex.item)
      
      self.collectionView.performBatchUpdates({
        
        let newIndexPaths = Array(repeating: destinationIndex,count: images.count)
        self.collectionView.insertItems(at: newIndexPaths)
        
      })
      
      
    }
    
  }

I got crashes on self.collectionView.performBatchUpdates({
Looking into finished versions of the book i found this:
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {

let destinationIndex = coordinator.destinationIndexPath?.item ?? 0

for item in coordinator.items {
  
  if coordinator.session.localDragSession != nil,
    let sourceIndex = item.sourceIndexPath?.item {

    self.entry?.images.remove(at: sourceIndex)
  }

  item.dragItem.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
    
    guard let image = object as? UIImage, error == nil else {
      print(error ?? "Error: object is not UIImage")
      return
    }
    
    DispatchQueue.main.async {
      self.entry?.images.insert(image, at: destinationIndex)
      self.reloadSnapshot(animated: true)
    }
    
  }
}

}

I think the the problem is on the way the chapter covers this, if you test and try to do drag/drop finishing chapter 3 the app will crash, please check, seems that other people have problems like this.

same problem for me

Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: 'UICollectionView must be updated via the UICollectionViewDiffableDataSource APIs when acting as the UICollectionView’s dataSource: please do not call mutation APIs directly on UICollectionView

please fix it in next update

ok I found it, please don’t call performBatchUpdates on the UICollectionView, instead just call the reloadSnapshot method

self.reloadSnapshot(animated: true)

in this case the destination index won’t be used since we are using the diffable API,

I don’t think you can mix both, if you want to place the drop item as the required destination index, we would have to not use the Diffable data source, correctly if I am wrong!

@marinbenc Can you please help with this when you get a chance? Thank you - much appreciated! :]

1 Like

Hi @giguerea and @wilsonilo

The main problem comes from the following method:

func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
  if session.localDragSession != nil {
    return UICollectionViewDropProposal(
          operation: .move,
          intent: .insertAtDestinationIndexPath)
  } else {
    return UICollectionViewDropProposal(
          operation: .copy,
          intent: .insertAtDestinationIndexPath)
  }
}

It seems that Apple never intended (or simply never tested) this method to be used together with diffable data sources. This was an issue at the time of writing the book, and still isn’t fixed.

That’s why you’ll see the starter project for Chapter 3 doesn’t use diffable data sources (if you look in EntryTableViewController.swift).

If you want to use drag and drop with diffable data sources, you can use the following code, as @giguerea already mentioned:

Use UIDropInteractionDelegate for when you want to add simple dropping to any view:

extension EntryTableViewController: UIDropInteractionDelegate {
  
  func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
    session.canLoadObjects(ofClass: UIImage.self)
  }
  
  func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
    UIDropProposal(operation: .copy)
  }
  
  func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
    session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
      guard let self = self else { return }
      let images = imageItems as! [UIImage]
      self.entry?.images.insert(contentsOf: images, at: 0)
      self.reloadSnapshot(animated: true)
    }
  }
}

Or, use UICollectionViewDropDelegate to add drag and drop support to a collection view:

extension EntryTableViewController: UICollectionViewDropDelegate {
  
  func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
    session.canLoadObjects(ofClass: UIImage.self)
  }
  
  func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
    
    let destinationIndex = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)
    
    // Remove rearranged items
    if coordinator.session.localDragSession != nil {
      for item in coordinator.items {
        guard let sourceIndex = item.sourceIndexPath else {
          return
        }
        
        self.entry?.images.remove(at: sourceIndex.item)
        // Don't reload the snapshot yet, we still have work to do
      }
    }
    
    // Add all items back in
    coordinator.session.loadObjects(ofClass: UIImage.self) {
      [weak self] imageItems in
      
      guard let self = self else { return }
      let images = imageItems as! [UIImage]
      
      self.entry?.images.insert(
        contentsOf: images,
        at: destinationIndex.item)
      self.reloadSnapshot(animated: true)
    }
  }
}

Here’s what reloadSnapshot does:

private func reloadSnapshot(animated: Bool) {
  var snapshot = NSDiffableDataSourceSnapshot<Int, UIImage>()
  snapshot.appendSections([0])
  snapshot.appendItems(entry?.images ?? [])
  dataSource?.apply(snapshot, animatingDifferences: animated)
}

Since you’ll get a crash if you use the method mentioned above that returns a drop proposal, you’ll either have to make do without animations or build your own custom ones in UIDropInteractionDelegate’s method dropInteraction(_:sessionDidUpdate:).

To summarize:

  • Drag and drop works with diffable data sources, and is described above.
  • However, using UICollectionViewDropDelegate to return a UICollectionViewDropProposal doesn’t, since iOS calls non-diffable methods under the hood. If you want to use the proposal, stick to working without diffable data sources.

I hope this helps!

2 Likes

I appreciate very much the response, i wish this was explained or better detailed on the book, or at least a link to an article to read more and understand it. But with this i can double check my code, thanks again!