简体   繁体   English

自定义集合视图布局崩溃

[英]Custom collection view layout crashes

I've created a custom data grid. 我创建了一个自定义数据网格。 It's based on collection view with custom layout. 它基于具有自定义布局的集合视图。 The layout modifies the first section and row attributes making them sticky, so when the user scrolls other rows and sections should go under the sticky ones. 该布局修改了第一节和行属性,使其具有粘性,因此,当用户滚动其他行和节时,它们应位于粘性之下。 The idea for this layout is not mine, I've just adopted it. 这种布局的想法不是我的,我只是采用了它。 ( I can't give the credits for the real creator, in my research I found so many variations of the layout that I'm not sure which is the original one ). 我不能为真正的创作者付出功劳,在我的研究中,我发现布局的变化太多,因此我不确定哪一个是原始布局 )。

Unfortunately I'm facing a problem with it. 不幸的是,我面临着一个问题。 I'm getting a crash when scrolling: 滚动时出现崩溃:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no UICollectionViewLayoutAttributes instance for -layoutAttributesForItemAtIndexPath: ***由于未捕获的异常“ NSInternalInconsistencyException”而终止应用程序,原因:“-layoutAttributesForItemAtIndexPath没有UICollectionViewLayoutAttributes实例:

Despite the message I think that the real problem is in layoutAttributesForElements method. 尽管有该消息,但我认为真正的问题出在layoutAttributesForElements方法中。 I've read some threads with a similar problem, but the only working solution is to return all cached attributes, no matter of the passed rectangle. 我已经阅读了一些具有类似问题的线程,但是唯一可行的解​​决方案是返回所有缓存的属性,无论传递的矩形如何。 I just don't like quick and dirty solutions like this. 我只是不喜欢这种快速又肮脏的解决方案。 I would really appreciate any ideas/solutions you can give me. 我真的很感激您能给我的任何想法/解决方案。

The whole project is here . 整个项目在这里 However the most important is the layout so for convenience here it is: 但是,最重要的是布局,因此为了方便起见,它是:

class GridViewLayout: UICollectionViewLayout {

    //MARK: - Setup

    private var isInitialized: Bool = false

    //MARK: - Attributes

    var attributesList: [[UICollectionViewLayoutAttributes]] = []

    //MARK: - Size

    private static let defaultGridViewItemHeight: CGFloat = 47
    private static let defaultGridViewItemWidth: CGFloat = 160

    static let defaultGridViewRowHeaderWidth: CGFloat = 200
    static let defaultGridViewColumnHeaderHeight: CGFloat = 80

    static let defaultGridViewItemSize: CGSize =
        CGSize(width: defaultGridViewItemWidth, height: defaultGridViewItemHeight)

    // This is regular cell size
    var itemSize: CGSize = defaultGridViewItemSize

    // Row Header Size
    var rowHeaderSize: CGSize =
        CGSize(width: defaultGridViewRowHeaderWidth, height: defaultGridViewItemHeight)

    // Column Header Size
    var columnHeaderSize: CGSize =
        CGSize(width: defaultGridViewItemWidth, height: defaultGridViewColumnHeaderHeight)

    var contentSize : CGSize!

    //MARK: - Layout

    private var columnsCount: Int = 0
    private var rowsCount: Int = 0

    private var includesRowHeader: Bool = false
    private var includesColumnHeader: Bool = false

    override func prepare() {
        super.prepare()

        rowsCount = collectionView!.numberOfSections
        if rowsCount == 0 { return }
        columnsCount = collectionView!.numberOfItems(inSection: 0)

        // make header row and header column sticky if needed
        if self.attributesList.count > 0 {
            for section in 0..<rowsCount {
                for index in 0..<columnsCount {
                    if section != 0 && index != 0 {
                        continue
                    }

                    let attributes : UICollectionViewLayoutAttributes =
                        layoutAttributesForItem(at: IndexPath(forRow: section, inColumn: index))!

                    if includesColumnHeader && section == 0 {
                        var frame = attributes.frame
                        frame.origin.y = collectionView!.contentOffset.y
                        attributes.frame = frame
                    }

                    if includesRowHeader && index == 0 {
                        var frame = attributes.frame
                        frame.origin.x = collectionView!.contentOffset.x
                        attributes.frame = frame
                    }
                }
            }

            return // no need for futher calculations
        }

        // Read once from delegate
        if !isInitialized {
            if let delegate = collectionView!.delegate as? UICollectionViewDelegateGridLayout {

                // Calculate Item Sizes
                let indexPath = IndexPath(forRow: 0, inColumn: 0)
                let _itemSize = delegate.collectionView(collectionView!,
                                                        layout: self,
                                                        sizeForItemAt: indexPath)

                let width = delegate.rowHeaderWidth(in: collectionView!,
                                                    layout: self)
                let _rowHeaderSize = CGSize(width: width, height: _itemSize.height)

                let height = delegate.columnHeaderHeight(in: collectionView!,
                                                         layout: self)
                let _columnHeaderSize = CGSize(width: _itemSize.width, height: height)

                if !__CGSizeEqualToSize(_itemSize, itemSize) {
                    itemSize = _itemSize
                }

                if !__CGSizeEqualToSize(_rowHeaderSize, rowHeaderSize) {
                    rowHeaderSize = _rowHeaderSize
                }

                if !__CGSizeEqualToSize(_columnHeaderSize, columnHeaderSize) {
                    columnHeaderSize = _columnHeaderSize
                }

                // Should enable sticky row and column headers
                includesRowHeader = delegate.shouldIncludeHeaderRow(in: collectionView!)
                includesColumnHeader = delegate.shouldIncludeHeaderColumn(in: collectionView!)
            }

            isInitialized = true
        }

        var column = 0
        var xOffset : CGFloat = 0
        var yOffset : CGFloat = 0
        var contentWidth : CGFloat = 0
        var contentHeight : CGFloat = 0

        for section in 0..<rowsCount {
            var sectionAttributes: [UICollectionViewLayoutAttributes] = []
            for index in 0..<columnsCount {
                var _itemSize: CGSize = .zero

                switch (section, index) {
                case (0, 0):
                    switch (includesRowHeader, includesColumnHeader) {
                    case (true, true):
                        _itemSize = CGSize(width: rowHeaderSize.width, height: columnHeaderSize.height)
                    case (false, true): _itemSize = columnHeaderSize
                    case (true, false): _itemSize = rowHeaderSize
                    default: _itemSize = itemSize
                    }
                case (0, _):
                    if includesColumnHeader {
                        _itemSize = columnHeaderSize
                    } else {
                        _itemSize = itemSize
                    }

                case (_, 0):
                    if includesRowHeader {
                        _itemSize = rowHeaderSize
                    } else {
                        _itemSize = itemSize
                    }
                default: _itemSize = itemSize
                }

                let indexPath = IndexPath(forRow: section, inColumn: index)
                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)

                attributes.frame = CGRect(x: xOffset,
                                          y: yOffset,
                                          width: _itemSize.width,
                                          height: _itemSize.height).integral

                // allow others cells to go under
                if section == 0 && index == 0 { // top-left cell
                    attributes.zIndex = 1024
                } else if section == 0 || index == 0 {
                    attributes.zIndex = 1023 // any ohter header cell
                }

                // sticky part - probably just in case here
                if includesColumnHeader && section == 0 {
                    var frame = attributes.frame
                    frame.origin.y = collectionView!.contentOffset.y
                    attributes.frame = frame
                }

                if includesRowHeader && index == 0 {
                    var frame = attributes.frame
                    frame.origin.x = collectionView!.contentOffset.x
                    attributes.frame = frame
                }

                sectionAttributes.append(attributes)

                xOffset += _itemSize.width
                column += 1

                if column == columnsCount {
                    if xOffset > contentWidth {
                        contentWidth = xOffset
                    }

                    column = 0
                    xOffset = 0
                    yOffset += _itemSize.height
                }
            }

            attributesList.append(sectionAttributes)
        }

        let attributes = self.attributesList.last!.last!

        contentHeight = attributes.frame.origin.y + attributes.frame.size.height
        self.contentSize = CGSize(width: contentWidth,
                                  height: contentHeight)

    }

    override var collectionViewContentSize: CGSize {
        return self.contentSize
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        var curLayoutAttribute: UICollectionViewLayoutAttributes? = nil

        if indexPath.section < self.attributesList.count {
            let sectionAttributes = self.attributesList[indexPath.section]

            if indexPath.row < sectionAttributes.count {
                curLayoutAttribute = sectionAttributes[indexPath.row]
            }
        }

        return curLayoutAttribute
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var attributes: [UICollectionViewLayoutAttributes] = []
        for section in self.attributesList {
            let filteredArray  =  section.filter({ (evaluatedObject) -> Bool in
                return rect.intersects(evaluatedObject.frame)
            })

            attributes.append(contentsOf: filteredArray)
        }

        return attributes
    }

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

    //MARK: - Moving

    override func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath,
                                                             withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes {
        guard let dest = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return UICollectionViewLayoutAttributes() }

        dest.transform = CGAffineTransform(scaleX: 1.4, y: 1.4)
        dest.alpha = 0.8
        dest.center = position

        return dest
    }

    override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath],
                                      withTargetPosition targetPosition: CGPoint,
                                      previousIndexPaths: [IndexPath],
                                      previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {
        let context =  super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths,
                                                 withTargetPosition: targetPosition,
                                                 previousIndexPaths: previousIndexPaths,
                                                 previousPosition: previousPosition)

        collectionView!.dataSource?.collectionView?(collectionView!,
                                                    moveItemAt: previousIndexPaths[0],
                                                    to: targetIndexPaths[0])

        return context
    }

} 

Implement layoutAttributesForItemAtIndexPath . 实现layoutAttributesForItemAtIndexPath As per the documentation, "Subclasses must override this method and use it to return layout information for items in the collection view. ". 根据文档,“子类必须重写此方法,并使用它返回集合视图中项目的布局信息。”。

In my experience this method is normally not called when running in the simulator but can be called on the device. 以我的经验,在模拟器中运行时通常不会调用此方法,但可以在设备上调用该方法。 YMMV. 因人而异。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM