UICollectionViewDropDelegate 和 DiffableDataSource

Posted

技术标签:

【中文标题】UICollectionViewDropDelegate 和 DiffableDataSource【英文标题】:UICollectionViewDropDelegate and DiffableDataSource 【发布时间】:2020-04-29 03:59:31 【问题描述】:

我正在尝试使用 DiffableDataSource(在 ios 13 中引入)实现 UICollectionViewDropDelegate

我已经实现了UICollectionViewDragDelegate,效果很好。 我设置collectionView.dropDelegate = self 我实现了func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)(你甚至可以在方法为空的情况下尝试)。

应用程序将构建并运行,但一旦您开始拖动单元格,应用程序就会立即崩溃并显示以下消息:

由于未捕获的异常“NSInternalInconsistencyException”而终止应用,原因:“作为 UICollectionView 的数据源时,必须通过 UICollectionViewDiffableDataSource API 更新 UICollectionView:请不要直接在 UICollectionView 上调用突变 API。

所以看起来这个方法试图直接修改 UICollectionView(也许在移动项目时移动单元格?)我不知道如何绕过这种行为。

想法?

【问题讨论】:

它们可能真的不兼容。在我看来,关于 diffable 数据源的很多内容都没有经过深思熟虑。 你有没有设法解决这个问题?我现在也有同样的 @matt 我认为他们只是忘记了拖放。组合式布局也不能很好地支持它。 【参考方案1】:

这是一个相当容易解决的问题。从 UICollectionViewDropDelegate 函数 performDropWith 开始。请注意,我只需要 .move 而不需要 .copy,但您只需添加一个类似于以下 reorderItems() 方法的 copyItems() 方法:

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) 
   var destinationIndexPath: IndexPath
   if let indexPath = coordinator.destinationIndexPath 
      destinationIndexPath = indexPath
    else 
      let section = collectionView.numberOfSections - 1
      let row = collectionView.numberOfItems(inSection: section)
      destinationIndexPath = IndexPath(row: row, section: section)
   
   switch coordinator.proposal.operation 
   case .move:
      self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
      break
   case .copy:
      // Not copying between collections so this block not needed.
      return
   default:
      return
   

然后我们有了处理collectionView 中实际更改所需的函数reorderItems()。需要明确的一点是快照 (NSDiffableDataSourceSnapshot) 和数据源 (UICollectionViewDiffableDataSource) 都是类变量。

/// reorderItems method
/// This method moves a cell from the sourceIndexPath to the destinationIndexPath within the same UICollectionView
///
/// - Parameters:
///   - coordinator: UICollectionViewDropCoordinator obtained in performDropWith
///   - destinationIndexPath: IndexPath where user dropped the element.
///   - collectionView: UICollectionView object where reordering is done.
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 
      var destIndexPath = destinationIndexPath
      if destIndexPath.row >= collectionView.numberOfItems(inSection: destIndexPath.section) 
         destIndexPath.row = collectionView.numberOfItems(inSection: destIndexPath.section) - 1
      

      /// Since my collectionView data is attributed to a Firebase.storage set of data, this is where I write my changes back to the store.

      snapshot.moveItem(dataSource.itemIdentifier(for: sourceIndexPath)!, beforeItem: dataSource.itemIdentifier(for: destinationIndexPath)!)
      dataSource.apply(snapshot, animatingDifference: true)
      coordinator.drop(items.first!.dragItem, toItemAt: destIndexPath)
   

【讨论】:

【参考方案2】:

如果您使用 collectionView.performBatchUpdates() 进行更改以更新您的 collectionView,这将崩溃,您似乎需要在数据源 (UICollectionViewDiffableDataSource) 上处理快照,如您所见,错误中指出:“UICollectionViewDiffableDataSource APIs作为 UICollectionView 的 dataSource 时:请不要直接在 UICollectionView 上调用突变 API"

试试这个:

// MARK: - Properties
  var dataSource: UICollectionViewDiffableDataSource<Int, UIImage>?

  var entry: Entry? 
    didSet 
      guard let entry = entry else  return 
      let dateFormatter = DateFormatter()
      dateFormatter.setLocalizedDateFormatFromTemplate("MMM dd yyyy, hh:mm")
      title = dateFormatter.string(from: entry.dateCreated)
    
  

你的条目是一个结构:

struct Entry 
  let id = UUID().uuidString
  let dateCreated = Date()
  var log: String?
  var images: [UIImage] = []
  var isFavorite: Bool = false


extension Entry: Hashable 
  func hash(into hasher: inout Hasher) 
    hasher.combine(dateCreated)
    hasher.combine(log)
  

  static func == (lhs: Entry, rhs: Entry) -> Bool 
    return lhs.dateCreated == rhs.dateCreated &&
      lhs.log ?? "" == rhs.log ?? "" &&
      lhs.images == rhs.images &&
      lhs.isFavorite == rhs.isFavorite
  

而 reloadSnapshot 是:

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

终于在你的 UICollectionViewDropDelegate 上:

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)
    
  


我的所有这些都是基于阅读和工作 raywenderlich.com “Catalyst by Tutorials”

【讨论】:

【参考方案3】:

还有一件事:

如果您使用UICollectionViewDropDelegate 方法返回UICollectionViewDropProposalcollectionView(_:dropSessionDidUpdate:withDestinationIndexPath destinationIndexPath:),则此方法将在为drop 设置动画时在后台调用非diffable 方法。

我的猜测是,Apple 从未将其与可区分的数据源一起测试过。您需要删除此方法并以不同的方式自己实现其动画。

【讨论】:

以上是关于UICollectionViewDropDelegate 和 DiffableDataSource的主要内容,如果未能解决你的问题,请参考以下文章