【问题标题】:Rearranging different-sized items of UICollectionView with UICollectionViewFlowLayout使用 UICollectionViewFlowLayout 重新排列不同大小的 UICollectionView 项
【发布时间】:2019-01-01 07:21:41
【问题描述】:

假设我有一个带有 UICollectionViewFlowLayout 的 UICollectionView,并且我的项目大小不同。所以我实现了collectionView(_:layout:sizeForItemAt:)

现在假设我允许用户重新排列项目 (collectionView(_:canMoveItemAt:))。

这就是问题所在。当一个单元格被拖动并且其他单元格被移开时,collectionView(_:layout:sizeForItemAt:) 被重复调用。但显然它要求错误的索引路径:一个单元格的大小是根据它被视觉移动到的位置的索引路径。因此,它在拖动过程中采用了错误的尺寸,因为它穿梭到不同的位置。

一旦拖动结束并调用collectionView(_:moveItemAt:to:),我更新数据模型并重新加载数据,所有单元格都将呈现正确的大小。该问题仅在拖动过程中出现。

collectionView(_:layout:sizeForItemAt:) 显然没有为我们提供足够的信息,无法知道在拖拽过程中返回什么答案。或者我应该说,我们被要求提供错误索引路径的大小。

我的问题是:人们到底在做什么?

【问题讨论】:

  • 您的回答(尽管很聪明)是不必要的破解。将this answer 视为已接受的答案。

标签: ios uicollectionview


【解决方案1】:

诀窍在于实现

override func collectionView(_ collectionView: UICollectionView, 
    targetIndexPathForMoveFromItemAt orig: IndexPath, 
    toProposedIndexPath prop: IndexPath) -> IndexPath {

在拖动过程中,该方法会被重复调用,但会出现一个单元格与另一个单元格交叉的时刻,并且单元格会被推开以进行补偿。此时,origprop 具有不同的值。因此,此时您需要根据单元格的移动方式修改所有尺寸。

为此,您需要在重新排列尺寸时模拟界面在单元格移动时所做的事情。运行时对此没有任何帮助!

这是一个简单的例子。假设用户只能在同一部分内移动单元格。并假设我们的数据模型看起来像这样,一旦collectionView(_:layout:sizeForItemAt:) 最初计算了它,每个 Item 都会记住它自己的大小:

struct Item {
    var size : CGSize
    // other stuff
}
struct Section {
    var itemData : [Item]
    // other stuff
}
var sections : [Section]!

sizeForItemAt: 将计算出的尺寸记忆到模型中的方式如下:

func collectionView(_ collectionView: UICollectionView, 
layout collectionViewLayout: UICollectionViewLayout, 
sizeForItemAt indexPath: IndexPath) -> CGSize {
    let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
    if memosize != .zero {
        return memosize
    }
    // no memoized size; calculate it now
    // ... not shown ...
    self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
    return sz
}

然后,当我们听说用户以一种使单元格移动的方式拖动时,我们读取了该部分的所有 size 值,执行与界面相同的删除和插入操作,然后放入重新排列的 size 值回到模型中:

override func collectionView(_ collectionView: UICollectionView, 
targetIndexPathForMoveFromItemAt orig: IndexPath, toProposedIndexPath 
prop: IndexPath) -> IndexPath {
    if orig.section != prop.section {
        return orig
    }
    if orig.item == prop.item {
        return prop
    }
    // they are different, we're crossing a boundary - shift size values!
    var sizes = self.sections[orig.section].rowData.map{$0.size}
    let size = sizes.remove(at: orig.item)
    sizes.insert(size, at:prop.item)
    for (ix,size) in sizes.enumerated() {
        self.sections[orig.section].rowData[ix].size = size
    }
    return prop
}

结果是collectionView(_:layout:sizeForItemAt:) 现在在拖动过程中给出了正确的结果。

额外的难题是,当拖动开始时,您需要保存所有原始尺寸,而当拖动结束时,您需要将它们全部恢复, 这样当拖动 结束 时结果也是正确的。

【讨论】:

  • 我不得不将我的自定义布局转换为UICollectionViewFlowLayout。仅仅改变我的超类是不够的,我必须完全接受流布局。幸运的是,这对我的情况有用。很想知道如何获得不同的拖动大小以使用完全自定义的UICollectionViewLayout
  • 但是,如果我不记忆大小并让 sizeForItem 方法为新的单元格排列计算它,为什么 collectionView 不会调整拖动的单元格的大小?能给个提示吗?
  • 我知道为什么会发生这种情况!对我来说不是那么明显。
【解决方案2】:

根据 Matts 的回答,我已经修改了代码以适应 UICollectionViewDiffableDataSource

  1. 在移动单元格的同时跟踪 indexPath:
/// Stores remapped indexPaths during reordering of cells
var changedIndexPaths = [IndexPath: IndexPath]()

func collectionView(_ collectionView: UICollectionView,
                    targetIndexPathForMoveFromItemAt orig: IndexPath,
                    toProposedIndexPath prop: IndexPath) -> IndexPath {
    guard orig.section == prop.section else { return orig }
    guard orig.item != prop.item else { return prop }

    let currentOrig = changedIndexPaths[orig]
    let currentProp = changedIndexPaths[prop]
    
    changedIndexPaths[orig] = currentProp ?? prop
    changedIndexPaths[prop] = currentOrig ?? orig
    
    return prop
}
  1. 计算单元格大小:
 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // remap path while moving cells or use indexPath   
    let usedPath = changedIndexPaths[indexPath] ?? indexPath 
        
    guard let data = dataSource.itemIdentifier(for: usedPath) else {
          return CGSize()
    }

    //  Calculate your size for usedPath here and return it
    // ...
    return size
}
  1. 在单元格的最终移动完成后重置 indexPath 映射 (changedIndexPaths):
 class DataSource: UICollectionViewDiffableDataSource<Int, Data> {

    /// Is called after an cell item was successfully moved
    var didMoveItemHandler: ((_ sourceIndexPath: IndexPath, _ target: IndexPath) -> Void)?
    
    override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        didMoveItemHandler?(sourceIndexPath, destinationIndexPath)
    }
}

dataSource.didMoveItemHandler = { [weak self] (source, destination) in
     self?.dataController.reorderObject(sourceIndexPath: source, destinationIndexPath: destination)
     self?.resetProposedIndexPaths()
}

func resetProposedIndexPaths() {
   changedIndexPaths = [IndexPath: IndexPath]() // reset
}

【讨论】:

    【解决方案3】:

    虽然公认的答案非常聪明(向你致敬,Matt ?),但它实际上是一种不必要的精心设计。有一个更简单的解决方案。

    关键是:

    1. 将像元大小存储在数据本身中。
    2. 在“移动”单元格进入新的 indexPath 时(而不是在单元格完成移动时)处理或“重新排列”数据。
    3. 直接从数据中获取单元格大小(现在已正确排列)。

    这就是它的样子......

    // MARK: UICollectionViewDataSource
    
    override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        // (This method will be empty!)
        // As the Docs states: "You must implement this method to support 
        // the reordering of items within the collection view." 
        // However, its implementation should be empty because, as explained
        // in (2) from above, we do not want to manipulate our data when the
        // cell finishes moving, but at the exact moment it enters a new
        // indexPath.
    }
    
    override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
            
        // This will be true at the exact moment the "moving" cell enters
        // a new indexPath.
        if originalIndexPath != proposedIndexPath {
    
            // Here, we rearrange our data to reflect the new position of
            // our cells.
            let removed = myDataArray.remove(at: originalIndexPath.item)
            myDataArray.insert(removed, at: proposedIndexPath.item)
        }
            
        return proposedIndexPath
    }
        
    // MARK: UICollectionViewDelegateFlowLayout
        
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // Finally, we simply fetch cell sizes from the properly arranged
        // data.
        let myObject = myDataArray[indexPath.item]
        return myObject.size
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-21
      • 1970-01-01
      • 2015-04-04
      • 2014-06-02
      相关资源
      最近更新 更多