ios - 如何使用动态大小的单元格在 UICollectionViewFlowLayout 子类上正确设置部分插入

标签 ios swift uicollectionview

我的目标是在 UICollectionView 中拥有动态大小的单元格。此外,当UICollectionView的可滚动contentWidth小于集合的bounds时,我希望项目在集合内居中。

Goal

到目前为止的方法:

我正在尝试以尽可能最干净的方式做到这一点。我有自动调整大小的 UICollectionViewCells,其中自动布局可以根据内容的约束确定单元格的大小。为此,我的单元格重写了 preferredLayoutAttributesFitting 函数,返回其内容的适当宽度。

我的自定义 UICollectionViewFlowLayout 子类使用 UICollectionViewCell 提供的宽度信息来正确调整单元格的大小。

在我的自定义 UICollectionViewFlowLayout 上重写的唯一方法是:

  • layoutAttributesForItem(at...)
  • layoutAttributesForElementsIn(矩形...)
  • shouldInvalidateLayout(forBoundsChange...)

UICollectionViewCells 的动态大小调整工作完美。

问题:

使用这种方法,我事先不知道每个单元格的大小(每个单元格可以不同)。我想实现 UICollectionViewdelegate 方法来提供一个插图,使我的内容在 Collection View 中居中。为此,我想我应该实现并覆盖

collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets

问题在于 inset 方法是在布局过程的早期调用的,我不知道布局期间将确定的 contentSize

以下是单个完整布局过程的各种布局方法的堆栈,按它们运行的​​顺序排列:

prepare started
prepare finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForElementsInRect finished
prepare started
insetForSectionAt 0 called <--- The datasource has knowledge of the presence of cells but the cells themselves are not yet dequeued
prepare finished
layoutAttributesForElementsInRect started
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished
layoutAttributesForElementsInRect started
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished
prepare started
prepare finished
layoutAttributesForElementsInRect started
<--- At this point Cells DO return valid sizes through Autolayout
layoutAttributesForItemAt [0, 0] started
layoutAttributesForItemAt [0, 0] finished
layoutAttributesForItemAt [0, 1] started
layoutAttributesForItemAt [0, 1] finished
layoutAttributesForItemAt [0, 2] started
layoutAttributesForItemAt [0, 2] finished
layoutAttributesForElementsInRect finished

对于单个布局过程,会多次调用各种布局方法,但只有在最后才能将来自不同单元格的自动布局的大小信息传递到布局。

我注意到,在单元格正确出列并允许运行其自动布局约束之前,sectionInset 委托(delegate)函数被提前调用。因此,在调用时不可能告知 inset 正确的值。

使用这种方法,是否可以在布局传递后强制调用重新计算 sectionInsetAt 或者我应该完全更改技术?

我尝试过的事情:

我尝试从 insetForSectionAt 委托(delegate)函数中查询 collectionViewLayoutcollectionViewContentSize。不幸的是,这不能在布局过程中使用,因为布局当前正在进行中。它会导致崩溃并出现错误:

[CollectionView] An attempt to update layout information was detected while already in the process of computing the layout (i.e. reentrant call). This will result in unexpected behaviour or a crash. This may happen if a layout pass is triggered while calling out to a delegate. UICollectionViewFlowLayout instance is (<App.ViewportToolbarFlowLayout: 0x7f8a5261add0>)

我尝试使用collectionViewcontentSize方法来获取insetForSectionAt委托(delegate)方法执行期间的大小,但它确实在布局过程中运行时不提供正确的数据。

最佳答案

在这里发布答案。我发现this answer作者:Alex Koshy,这对我帮助很大。

基本上,该工作必须在 layoutAttributesForElementsIn(rect...) 中执行,因为它是唯一可用的完整布局图片的地方。我已经调整了 Alex 的代码来处理水平和垂直的 UICollectionViews(这可能不是每个人都想要的,但正是我需要的)。我使用布局的 scrollDirection 属性来确定元素是水平居中(当 scrollDirection.horizo​​ntal 时)还是垂直居中(当 >scrollDirection.vertical)。

这是代码:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    guard let collectionView = collectionView else { return nil }

    guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
    let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute -> UICollectionViewLayoutAttributes? in
        return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
    }
    guard let attributes = NSArray(array: computedAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil }

    let interItemSpacing = minimumInteritemSpacing

    switch scrollDirection {
    case .horizontal:
        let leftPadding: CGFloat = self.sectionInset.left
        var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item
        var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item
        var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row
        var currentRow: Int = 0 // Tracks the current row
        attributes.forEach { layoutAttribute in
            // Each layoutAttribute represents its own item
            if layoutAttribute.frame.origin.y >= maxY {

                // This layoutAttribute represents the left-most item in the row
                leftMargin = leftPadding

                currentRow += rowSizes.isEmpty ? 0 : 1
                // Register its origin.x in rowSizes for use later
                rowSizes.append([leftMargin, 0])
            }
            layoutAttribute.frame.origin.x = leftMargin
            leftMargin += layoutAttribute.frame.width + interItemSpacing
            maxY = max(layoutAttribute.frame.maxY, maxY)
            // Add right-most x value for last item in the row
            rowSizes[currentRow][1] = leftMargin - interItemSpacing
        }
        // At this point, all cells are left aligned
        // Reset tracking values and add extra left padding to center align entire row
        leftMargin = leftPadding
        maxY = -1.0
        currentRow = 0
        attributes.forEach { (layoutAttribute) in
            if layoutAttribute.frame.origin.y >= maxY {

                // This layoutAttribute represents the left-most item in the row
                leftMargin = leftPadding

                // Need to bump it up by an appended margin
                let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x
                let appendedMargin = (collectionView.frame.width - leftPadding  - rowWidth - leftPadding) / 2
                leftMargin += appendedMargin

                currentRow += 1
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + interItemSpacing
            maxY = max(layoutAttribute.frame.maxY, maxY)
        }

    case .vertical:
        let topPadding: CGFloat = self.sectionInset.top
        var topMargin: CGFloat = topPadding
        var maxX: CGFloat = -1.0 // Modified to determine origine.x for each item
        var colSizes: [[CGFloat]] = [] // Tracks the starting and ending y-values for the first and last item in the column
        var currentCol: Int = 0
        attributes.forEach { (layoutAttribute) in
            if layoutAttribute.frame.origin.x >= maxX {
                topMargin = topPadding

                currentCol += colSizes.isEmpty ? 0 : 1
                colSizes.append([topMargin, 0])
            }

            layoutAttribute.frame.origin.y = topMargin
            topMargin += layoutAttribute.frame.height + interItemSpacing
            maxX = max(layoutAttribute.frame.maxX, maxX)
            colSizes[currentCol][1] = topMargin - interItemSpacing
        }
        topMargin = topPadding
        maxX = -1.0
        currentCol = 0
        attributes.forEach { (layoutAttribute) in
            if layoutAttribute.frame.origin.x >= maxX {

                topMargin = topPadding

                let colWidth = colSizes[currentCol][1] - colSizes[currentCol][0]
                let appendedMargin = (collectionView.frame.height - topPadding - colWidth - topPadding) / 2
                topMargin += appendedMargin

                currentCol += 1
            }

            layoutAttribute.frame.origin.y = topMargin

            topMargin += layoutAttribute.frame.height + interItemSpacing
            maxX = max(layoutAttribute.frame.maxX, maxX)
        }
    }

    return attributes
}

关于ios - 如何使用动态大小的单元格在 UICollectionViewFlowLayout 子类上正确设置部分插入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55078289/

相关文章:

ios - 我的 UICollectionViewController 中的单元格消失了

ios - NSFetchedResultsController 获取特定值

ios - 导航栏标题和后退按钮名称是否相同?

c# - 您如何声明类型为 'enum' 的参数(即它可以采用任何枚举)?

swift - 我在 Swift UI 结构中可以拥有的状态变量的最大数量是多少,性能会随着变量的增加而降低吗?

swift - Swift-显示带有动画的标题

ios - 以编程方式在带有商店的 ViewController 中添加两个 UICollectionView

ios - SpriteKit 内存管理预加载缓存和 fps 问题

ios - 如何在 Facebook 上发布路线图而不是图片

ios - 将字符串数组转换为 JSON 或 2D 数组 SWIFT