Crash while using UICollectionViewCompositionalLayout with pinned headers

Hi,

I've implemented a 2D Grid View using UICollectionViewCompositionalLayout.

Implementation Overview:

  • Each row in the grid represented by a NSCollectionLayoutSection which contains a single instance of NSCollectionLayoutGroup.

  • Each row also has a sticky header implemented using NSCollectionLayoutBoundarySupplementaryItem shown on leading edge of the row. It is set as a boundary item for the section. This row header is pinned to the visible bounds via pinToVisibleBounds property to make it sticky.

  • I've also created a global sticky header for the 2D Grid View by creating an array of NSCollectionLayoutBoundarySupplementaryItem instance items(one for each column of the grid) and adding them to overall layout via UICollectionViewCompositionalLayoutConfiguration. These grid view header items are all pinned to the visible bounds via pinToVisibleBounds property to make them sticky while scrolling through rows.

Although, it mostly worked with few tweaks by overriding UICollectionLayout methods, i am running into a random crash while scrolling through the grid view.

Here's the crash

2022-04-09 00:28:56.335279-0700 *****[88274:5668805] *** Assertion failure in CGRect _UIPinnedFrameForFrameWithContainerFrameVisibleFrame(CGRect, CGRect, CGRect, NSRectAlignment)(), _UICollectionLayoutHelpers.m:688
2022-04-09 00:28:56.341980-0700 ******[88274:5668805] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Frame {{377, 0}, {0, 627}} does not intersect {{377, 0}, {0, 627}}'
*** First throw call stack:
(
	0  CoreFoundation           0x000000010aa45d44 __exceptionPreprocess + 242
	1  libobjc.A.dylib           0x0000000107671a65 objc_exception_throw + 48
	2  Foundation             0x00000001089357d9 -[NSMutableDictionary(NSMutableDictionary) classForCoder] + 0
	3  UIKitCore              0x00000001187a09cb _UIPinnedNonOverlappingFramesForContentFrameVisibleFrame + 3154
	4  UIKitCore              0x00000001187b7ff4 -[_UICollectionLayoutAuxillaryItemSolver _solveForPinning:visibleRect:] + 5748
	5  UIKitCore              0x00000001187824ed -[_UICollectionCompositionalLayoutSolver updatePinnedSectionSupplementaryItemsForVisibleBounds:] + 723
	6  UIKitCore              0x000000011877e542 -[UICollectionViewCompositionalLayout _updatePinnedSectionSupplementaryItemsForCurrentVisibleBounds] + 381
	7  UIKitCore              0x000000011877f148 -[UICollectionViewCompositionalLayout _solveForPinnedSupplementaryItemsIfNeededWithContext:] + 121
	8  UIKitCore              0x000000011877a203 -[UICollectionViewCompositionalLayout invalidateLayoutWithContext:] + 802
	9  UIKitCore              0x000000011888770d -[UICollectionViewLayout _invalidateLayoutUsingContext:] + 56
	10 UIKitCore              0x00000001188202e5 -[UICollectionView setBounds:] + 757
	11 UIKitCore              0x0000000119713ab2 -[UIScrollView setContentOffset:] + 1047
	12 UIKitCore              0x0000000118839f2e -[UICollectionView setContentOffset:] + 42
	13 UIKitCore              0x0000000119728b6e -[UIScrollView _smoothScrollSyncWithUpdateTime:] + 3152
	14 UIKitCore              0x0000000119727b55 -[UIScrollView _smoothScrollWithUpdateTime:] + 313
	15 UIKitCore              0x0000000119728f06 -[UIScrollView _smoothScrollDisplayLink:] + 613
	16 QuartzCore             0x0000000113937474 _ZN2CA7Display11DisplayLink14dispatch_itemsEyyy + 932
	17 QuartzCore             0x0000000113a369c6 _ZL22display_timer_callbackP12__CFMachPortPvlS1_ + 395
	18 CoreFoundation           0x000000010a97eb42 __CFMachPortPerform + 157
	19 CoreFoundation           0x000000010a9b3125 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41
	20 CoreFoundation           0x000000010a9b24cc __CFRunLoopDoSource1 + 617
	21 CoreFoundation           0x000000010a9ac901 __CFRunLoopRun + 2420
	22 CoreFoundation           0x000000010a9aba90 CFRunLoopRunSpecific + 562
	23 GraphicsServices          0x0000000110617c8e GSEventRunModal + 139
	24 UIKitCore              0x00000001191e490e -[UIApplication _run] + 928
	25 UIKitCore              0x00000001191e9569 UIApplicationMain + 101
	26 SEAnalyticsSDKSample        0x0000000106ec8b05 main + 229
	27 dyld                0x0000000107140f21 start_sim + 10
	28 ???                 0x000000011063d51e 0x0 + 4569945374
)
libc++abi: terminating with uncaught exception of type NSException

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Frame {{377, 0}, {0, 627}} does not intersect {{377, 0}, {0, 627}}'
terminating with uncaught exception of type NSException

I'm trying to understand what is causing this crash and how to prevent this crash from happening..

Code is mentioned below

Here's the code:

func layout() -> UICollectionViewLayout {
    UICollectionViewCompositionalLayout(sectionProvider: { [self] row, _ in
      var cellItems: [NSCollectionLayoutItem] = []
      let rowHeight = NSCollectionLayoutDimension.absolute(44)
      var rowWidth: CGFloat = 0.0
       
      for column in 0..<10 {
        let cellWidth = NSCollectionLayoutDimension.absolute(100)
        let cellSize = NSCollectionLayoutSize(widthDimension: cellWidth, heightDimension: rowHeight)
        let cell = NSCollectionLayoutItem(layoutSize: cellSize)
        cellItems.append(cell)
        rowWidth += width
      }

      var rowBoundaryItems: [NSCollectionLayoutBoundarySupplementaryItem] = []
      var groupOffset: CGFloat = 0.0

      if let rowHeader = header(for: row) {
        groupOffset = 60
        rowBoundaryItems.append(rowHeader)
      }

      rowWidth += groupOffset
       
      let rowSize = NSCollectionLayoutSize(widthDimension: .absolute(rowWidth), heightDimension: rowHeight)
      let group = NSCollectionLayoutGroup.horizontal(layoutSize: rowSize, subitems: cellItems)
      group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: groupOffset, bottom: 0, trailing: 0)
      
      let section = NSCollectionLayoutSection(group: group)
      section.boundarySupplementaryItems = rowBoundaryItems
      return section
    }, configuration: configuration)
  }
     
  var configuration: UICollectionViewCompositionalLayoutConfiguration {
    let configuration = UICollectionViewCompositionalLayoutConfiguration()

    var boundaryItems: [NSCollectionLayoutBoundarySupplementaryItem] = []

    if let topLeftItem = topLeftView {
      boundaryItems.append(topLeftItem)
    }
     
    boundaryItems += columnHeaderItems

    configuration.boundarySupplementaryItems = boundaryItems
    return configuration
  }
   
  var topLeftView: NSCollectionLayoutBoundarySupplementaryItem? { 
    let columnHeaderHeight = NSCollectionLayoutDimension.absolute(44)
    let topLeftCellSize = NSCollectionLayoutSize(widthDimension: NSCollectionLayoutDimension.absolute(60, heightDimension: columnHeaderHeight)

    let topLeftCell = NSCollectionLayoutBoundarySupplementaryItem(
      layoutSize: topLeftCellSize,
      elementKind: "topLeftHeader",
      alignment: .topLeading
    )
     
    topLeftCell.pinToVisibleBounds = true
    topLeftCell.zIndex = 3
     
    return topLeftCell
  }
   
  var columnHeaderItems: [NSCollectionLayoutBoundarySupplementaryItem] {
    var headerItems: [NSCollectionLayoutBoundarySupplementaryItem] = []
    var offset = 60
     
    for column in 0..<datasource.numberOfColumns {
      let width = dimensionsProvider.widthForColumn(column)
      let columnHeaderSize = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .absolute(44))

      let headerView = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: columnHeaderSize,
        elementKind: "columnHeader",
        alignment: .topLeading,
        absoluteOffset: CGPoint(x: offset, y: 0)
      )

      headerView.pinToVisibleBounds = true
      headerView.zIndex = 2
       
      offset += width
      headerItems.append(headerView)
    }
     
    return headerItems
  }

  func header(for row: Int) -> NSCollectionLayoutBoundarySupplementaryItem? {
    let rowHeight = 44
    let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(60, heightDimension: .absolute(rowHeight))
    let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "rowHeader", alignment: .leading)
    header.pinToVisibleBounds = true
    header.zIndex = 1
     
    return header
  }

I narrowed down the root cause and its happening when you pin the NSCollectionLayoutBoundarySupplementaryItem that are applied to the layout through UICollectionViewCompositionalLayoutConfiguration ..

Here's the narrowed down code that's causing this crash..

 func layout() -> UICollectionViewLayout {
    UICollectionViewCompositionalLayout(sectionProvider: { [self] section, _ in
      // Building section here 
    }, configuration: configuration)
  }
     
  var configuration: UICollectionViewCompositionalLayoutConfiguration {
    let configuration = UICollectionViewCompositionalLayoutConfiguration()

    var boundaryItems: [NSCollectionLayoutBoundarySupplementaryItem] = []

    var offset = 0
    for column in 0..<datasource.numberOfColumns {
      let columnHeaderSize = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .absolute(44))

      let headerView = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: columnHeaderSize,
        elementKind: "columnHeader",
        alignment: .topLeading,
        absoluteOffset: CGPoint(x: offset, y: 0)
      )

      headerView.pinToVisibleBounds = true
      headerView.zIndex = 2
       
      offset += 100.0
      boundaryItems.append(headerView)
    } 

    configuration.boundarySupplementaryItems = boundaryItems
    return configuration
  }

Totally same here, waiting for solution.

Did anyone found a solution or at least a workaround for this?

Crash while using UICollectionViewCompositionalLayout with pinned headers
 
 
Q