Custom collection view layout crashes

Question

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:

Despite the message I think that the real problem is in layoutAttributesForElements method. 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
    }

} 

Show source
| xcode   | swift   | ios   | uicollectionview   | uicollectionviewlayout   2017-01-07 11:01 0 Answers

Answers to Custom collection view layout crashes ( 0 )

Leave a reply to - Custom collection view layout crashes

◀ Go back