Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView exception in UICollectionViewLayoutAttributes from iOS7

I have done a View in CollectionView with CustomLayout. In iOS6 it worked great but iOS7 it throws an exception like this.

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:

'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from CustomSupplementaryAttributes: 0xd1123a0 index path: (NSIndexPath: 0xd112580 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; to CustomSupplementaryAttributes: 0xd583c80 index path: (NSIndexPath: 0xd583c70 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; without invalidating the layout'

like image 971
Jirune Avatar asked Oct 06 '13 10:10

Jirune


6 Answers

iOS 10

At iOS 10, a new feature is introduced, it is Cell Prefetching. It will let dynamic position of SupplementaryView crash. In order to run in the old behavior, it needs to disable prefetchingEnabled. It's true by default at iOS 10.

// Obj-C
// This function is available in iOS 10. Disable it for dynamic position of `SupplementaryView `.
if ([self.collectionView respondsToSelector:@selector(setPrefetchingEnabled:)]) {
    self.collectionView.prefetchingEnabled = false;
}

// Swift
if #available(iOS 10, *) { 
    // Thanks @maksa
    collectionView.prefetchingEnabled = false 

    // Swift 3 style
    colView.isPrefetchingEnabled = false   
}

I hate this problem. I spend 2 days for this problem. A reference about Cell Pre-fetch @iOS 10.


iOS 9 and before ...

@Away Lin is right.. I solve the same problem by implementing that delegate method.

My Custom UICollectionViewLayout will modify the attributes in layoutAttributesForElementsInRect. The section position is dynamic, not static. So, I obtain warnings about the layout attributes for supplementary item at index path ... changed from ... to .... Before the changes, invalideLayout related methods should be called.

And, after implementing this delegate method to return true, the method invalidateLayoutWithContext: will be called when scrolling the UICollectionViewLayout. By default, it returns false.

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

From Apple Docs

Return Value true if the collection view requires a layout update or false if the layout does not need to change.

Discussion The default implementation of this method returns false. Subclasses can override it and return an appropriate value based on whether changes in the bounds of the collection view require changes to the layout of cells and supplementary views.

If the bounds of the collection view change and this method returns true, the collection view invalidates the layout by calling the invalidateLayoutWithContext: method.

Availability Available in iOS 6.0 and later.


And more ...

A nice example project on GitHub, for custom UICollectionViewLayout.

like image 180
AechoLiu Avatar answered Nov 14 '22 22:11

AechoLiu


You need to invalidate the existing layout before updating, see the end of the error message:

without invalidating the layout'

[collectionViewLayout invalidateLayout];

Apple Documentation for UICollectionViewLayout

like image 28
Tim Avatar answered Nov 14 '22 22:11

Tim


I had the same exception: in iOS 7, you need now to override the inherited isEqual: in your UICollectionViewLayoutAttributes subclass as stated in Apple documentation here.

like image 33
Spi Avatar answered Nov 14 '22 23:11

Spi


I solved my problem by override the method at the subclase of UICollectionViewFlowLayout:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound

return YES

like image 24
Away Lin Avatar answered Nov 15 '22 00:11

Away Lin


I'm not entirely certain how or why, but this appears to be fixed in iOS 12, supporting both supplementary view resizing and prefetching. The trick for me was to make sure things are happening in the correct order.

Here is a working implementation of a stretchable header view. Notice the implementation of the header resizing happening in layoutAttributesForElements(in rect: CGRect):

class StretchyHeaderLayout: UICollectionViewFlowLayout {

    var cache = [UICollectionViewLayoutAttributes]()

    override func prepare() {
        super.prepare()

        cache.removeAll()

        guard let collectionView = collectionView else { return }

        let sections = [Int](0..<collectionView.numberOfSections)
        for section in sections {
            let items = [Int](0..<collectionView.numberOfItems(inSection: section))
            for item in items {
                let indexPath = IndexPath(item: item, section: section)
                if let attribute = layoutAttributesForItem(at: indexPath) {
                    cache.append(attribute)
                }
            }
        }

        if let header = layoutAttributesForSupplementaryView(ofKind: StretchyCollectionHeaderKind, at: IndexPath(item: 0, section: 0)) {
            cache.append(header)
        }
    }

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

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let visibleAttributes = cache.filter { rect.contains($0.frame) || rect.intersects($0.frame) }

        guard let collectionView = collectionView else { return visibleAttributes }

        // Find the header and stretch it while scrolling.
        guard let header = visibleAttributes.filter({ $0.representedElementKind == StretchyCollectionHeaderKind }).first else { return visibleAttributes }
        header.frame.origin.y = collectionView.contentOffset.y
        header.frame.size.height = headerHeight.home - collectionView.contentOffset.y
        header.frame.size.width = collectionView.frame.size.width

        return visibleAttributes
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as! UICollectionViewLayoutAttributes
        guard collectionView != nil else { return attributes }

        attributes.frame.origin.y =  headerHeight.home + attributes.frame.origin.y

        return attributes
    }

    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: StretchyCollectionHeaderKind, with: indexPath)
    }

    override var collectionViewContentSize: CGSize {
        get {
            guard let collectionView = collectionView else { return .zero }

            let numberOfSections = collectionView.numberOfSections
            let lastSection = numberOfSections - 1
            let numberOfItems = collectionView.numberOfItems(inSection: lastSection)
            let lastItem = numberOfItems - 1

            guard let lastCell = layoutAttributesForItem(at: IndexPath(item: lastItem, section: lastSection)) else { return .zero }

            return CGSize(width: collectionView.frame.width, height: lastCell.frame.maxY + sectionInset.bottom)
        }
    }
}

P.S.: I'm aware the cache doesn't actually serve any purpose at this point :)

like image 26
brandonscript Avatar answered Nov 15 '22 00:11

brandonscript


I had this problem too, because I had code that depended on the content size of the collection view. My code was accessing the content size via the collectionView!.contentSize instead of collectionViewContentSize.

The former uses the collectionView property of UICollectionViewLayout, while the latter uses the custom-implemented layout property. In my code, the first time the layout was asked for attributes, contentSize had not been set yet.

like image 1
JSquared Avatar answered Nov 14 '22 23:11

JSquared