比方说,我有一个带有UICollectionViewFlowLayout的UICollectionView,并且我的项目大小不同。因此,我已经实现了collectionView(_:layout:sizeForItemAt:)
。
现在假设我允许用户重新排列项目(collectionView(_:canMoveItemAt:)
)。
这是问题所在。当一个单元格被拖动而其他单元格移开时,collectionView(_:layout:sizeForItemAt:)
被反复调用。但这显然是需要使用错误的索引路径的:单元格的大小与视觉上要移动到的位置的索引路径相同。因此,在拖动过程中,由于它穿梭到不同的位置而采用了错误的尺寸。
一旦拖动结束并调用collectionView(_:moveItemAt:to:)
,并且我更新了数据模型并重新加载了数据,则所有单元格都将假定其正确大小。该问题仅在拖动期间 出现。
很明显,collectionView(_:layout:sizeForItemAt:)
中我们没有得到足够的信息来知道拖动继续进行时要返回什么答案。或者也许我应该说,我们被要求提供错误索引路径的大小。
我的问题是:人们到底在做什么?
答案 0 :(得分:5)
诀窍是实施
override func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath,
toProposedIndexPath prop: IndexPath) -> IndexPath {
在拖动过程中,该方法被重复调用,但是有一瞬间,一个单元格越过另一个单元格,而其他单元格则被推开以进行补偿。此时,orig
和prop
具有不同的值。因此,此时您需要根据单元的移动方式来修改所有大小。
要做到这一点,您需要在重新排列大小时模拟单元在移动时界面正在执行的操作。运行时对此没有帮助!
这是一个简单的例子。假定用户只能在同一部分内移动单元格。并假设我们的数据模型如下所示,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
。
/// 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
}
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
}
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)
虽然接受的答案非常聪明(给你马特 ?),但它实际上是一个不必要的精心设计的黑客。有一个更简单的解决方案。
关键是:
这就是它的样子......
// 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
}