自定义 UICollectionViewLayout 和浮动补充 header 视图崩溃

Custom UICollectionViewLayout and floating supplementary header views crash

我正在尝试创建一个代表网格的 UICollectionView,我想在顶部边缘创建一个标尺(浮动 header 视图),该标尺在 [=48= 中浮动]. 所以,就像在 UITableViews 中一样,这个视图基本上应该只随着屏幕顶部的内容滚动。我希望描述足够清楚。理论说了这么多,让我们进入实践部分!

这是我现在正在使用的东西:

我有一个自定义 UICollectionViewLayout 具有以下内容 layoutAttributesForElementsInRect: 方法:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {

        var attribs = [UICollectionViewLayoutAttributes]()
        for (indexPath, attrs) in self.attributes{
            if CGRectIntersectsRect(rect, attrs.frame) {
                attribs.append(attrs)
            }
        }

        var headerAttributes = GridViewRulerAttributes(forSupplementaryViewOfKind: GridRulerView.Kind, withIndexPath: NSIndexPath(forItem: 0, inSection: 0))
        headerAttributes.zIndex = 500
        headerAttributes.frame = CGRectMake(0, self.collectionView!.contentOffset.y, self.preCalculatedContentSize.width, 80)
        attribs.append(headerAttributes)

        return attribs

}

当我启动应用程序时,一切看起来都很棒,正是我想要的。 但是一旦我将 collectionView 滚动到它的边界之外(collectionView 的 contentOffset = (-0.5,-0.5) <- 例如任何负值)我就会崩溃:

*** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit/UIKit-3318.16.25/UICollectionViewData.m:426

有趣的是,如果我将 header 视图属性的框架的 y 属性 设置为 0 而不是变量 contentOffset.y(变为负值),一切正常。

有人知道为什么会这样吗?

编辑

这是一条信息更丰富的错误消息:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 0; 425.227 24); zIndex = 500; to index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 -5; 425.227 24); zIndex = 500; without invalidating the layout'

为什么要为 ReuseIdentifier 传递 GridRulerView.Kind?

    reusableHeaderView = collectionView.dequeueReusableSupplementaryViewOfKind(GridRulerView.Kind, withReuseIdentifier: **GridRulerView.Kind**, forIndexPath: indexPath) as! UICollectionReusableView

应该如下所示。

var supplementaryView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier:Identifiers.HeaderIdentifier.rawValue, forIndexPath: indexPath) as UICollectionReusableView

哇,实际上禁用了 "All Exceptions" 断点并得到了一个信息更丰富的错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 0; 425.227 24); zIndex = 500; to index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 -5; 425.227 24); zIndex = 500; without invalidating the layout'

现在,我仍然不知道该做什么,或者去哪里

"invalidate the layout"

然后在浏览类似的问题时,我在 SO 上遇到了一个类似的问题,在那里我偶然发现了这个简单的函数:

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool{
    return true
}

果然,这似乎奏效了!没有崩溃了!

编辑

实际上,这导致集合视图布局在每次滚动时都无效,所以这似乎不是我正在寻找的解决方案...

编辑 #2

经过一些研究,我认为不可能创建一个在不使布局无效的情况下自行更改的补充视图(这看起来完全合乎逻辑 - 布局怎么知道只更新标尺在没有明确说明的情况下查看?)。并且更新每个卷轴上的布局太昂贵了。

所以我想我将创建一个完全独立于集合视图的单独视图,并简单地监听集合视图的滚动委托并根据滚动偏移调整其框架。我认为这种方法在性能方面效率更高,而且同样易于实施。

解决方案

只需检查 representedElementKind

class CustomFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributesForElementsInRect = super.layoutAttributesForElements(in: rect)
        var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()

        for attributes in attributesForElementsInRect! {

            if !(attributes.representedElementKind == UICollectionElementKindSectionHeader
                || attributes.representedElementKind == UICollectionElementKindSectionFooter) {

                // cells will be customise here, but Header and Footer will have layout without changes.
            }

            newAttributesForElementsInRect.append(attributes)
        }

        return newAttributesForElementsInRect
    }
}


class YourViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let headerNib = UINib.init(nibName: "HeaderCell", bundle: nil)
        collectionView.register(headerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")

        let footerNib = UINib.init(nibName: "FooterCell", bundle: nil)
        collectionView.register(footerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "FooterCell")
    }


    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

        switch kind {
        case UICollectionElementKindSectionHeader:
            let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderCell", for: indexPath) as! HeaderCell
            return headerView
        case UICollectionElementKindSectionFooter:
            let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! FooterCell
            return footerView
        default:
            return UICollectionReusableView()
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        return CGSize(width: collectionView.frame.width, height: 45)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        return CGSize(width: collectionView.frame.width, height: 25)
    }
}