使用UICollectionViewFlowLayout

时间:2018-07-24 15:31:44

标签: ios uicollectionview

比方说,我有一个带有UICollectionViewFlowLayout的UICollectionView,并且我的项目大小不同。因此,我已经实现了collectionView(_:layout:sizeForItemAt:)

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

这是问题所在。当一个单元格被拖动而其他单元格移开时,collectionView(_:layout:sizeForItemAt:)被反复调用。但这显然是需要使用错误的索引路径的:单元格的大小与视觉上要移动到的位置的索引路径相同。因此,在拖动过程中,由于它穿梭到不同的位置而采用了错误的尺寸。

enter image description here

一旦拖动结束并调用collectionView(_:moveItemAt:to:),并且我更新了数据模型并重新加载了数据,则所有单元格都将假定其正确大小。该问题仅在拖动期间 出现。

很明显,collectionView(_:layout:sizeForItemAt:)中我们没有得到足够的信息来知道拖动继续进行时要返回什么答案。或者也许我应该说,我们被要求提供错误索引路径的大小。

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

3 个答案:

答案 0 :(得分:5)

诀窍是实施

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

在拖动过程中,该方法被重复调用,但是有一瞬间,一个单元格越过另一个单元格,而其他单元格则被推开以进行补偿。此时,origprop具有不同的值。因此,此时您需要根据单元的移动方式来修改所有大小。

要做到这一点,您需要在重新排列大小时模拟单元在移动时界面正在执行的操作。运行时对此没有帮助!

这是一个简单的例子。假定用户只能在同一部分内移动单元格。并假设我们的数据模型如下所示,collectionView(_:layout:sizeForItemAt:)最初计算出每个项目后便会记住其自身大小:

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:)现在在拖动过程中给出了正确的结果。

另一个难题是,当拖动开始时,您需要保存所有原始大小,而当拖动结束时,您需要恢复所有大小,这样当拖动结束时,结果也将是正确的。

答案 1 :(得分:0)

根据 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
}

答案 2 :(得分:0)

虽然接受的答案非常聪明(给你马特 ?),但它实际上是一个不必要的精心设计的黑客。有一个更简单的解决方案。

关键是:

  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
}