NSCollectionView compositional layout with orthogonal scrolling has incorrect header constraints and other weirdness

43 Views Asked by At

I'm beginning to suspect NSCollectionView + Compositional Layout on macOS is very buggy ...

I'm using NSCollectionView + Compositional layout.

My layout is toggle-able between an orthogonal scrolling section or a more standard 'tiled' section.

Behavior I am noticing when I am the orthogonal scrolling mode is my section headers have NSLayoutConstraints automatically set by the compositional layout engine that have the wrong origin.

In this screenshot, I am in orthogonal scrolling mode.

You can see that sections which have a small number of items have the section header to beyond the leading edge of the scroll view. What is interesting, is origin adapts to the width of the scroll view.

enter image description here

The wider I made the view, the larger the offset to the left is:

enter image description here

Here is a non orthogonal scrolling screenshot which has expected header layouts with the right origin:

enter image description here

My Header view has translatesAutoresizingMaskIntoConstraints set to false, and uses constraints within it (and those constraints are correct, its just the origin of the scroll view which is off).

I've read other issues on here quirks of compositional layout

  • using vertical groups seems to fix some layout issues
  • Im not using estimated sizes here
  • the issue remains if I remove content insets
  • the issue remains if I remove my background views (in fact the background views are always correct even if the headers aren't)

Im running Xcode 15.1 on macOS 13.6.3 if that makes a difference,

Any insights or suggestions welcome.

My layout is as follows:

   func createLayout()
    {
        let config = NSCollectionViewCompositionalLayoutConfiguration()
        config.scrollDirection = .vertical
        config.interSectionSpacing = 16.0
        
        let layout = NSCollectionViewCompositionalLayout(sectionProvider: {
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            let snapshot = self.dataSource.snapshot()
            
            let width = layoutEnvironment.container.effectiveContentSize.width
            
            if self.layoutWrap
            {
                return self.mediaLayoutSection(snapshot:snapshot, forEnvironmentWidth: width)
            }
            else
            {
                return self.mediaLayoutSection(snapshot:snapshot, forEnvironmentHeight:150)
            }

        }, configuration: config)
        
        layout.register(OzuBackgroundRoundedView.self, forDecorationViewOfKind: OzuBackgroundRoundedView.kind)
        
        self.collectionView.collectionViewLayout = layout
    }
    
    private  func mediaLayoutSection(snapshot:NSDiffableDataSourceSnapshot<VideoAssetWrapper, SegmentEditViewWrapper>, forEnvironmentHeight:CGFloat) -> NSCollectionLayoutSection
    {
        let itemHeight =  floor( forEnvironmentHeight )
        
        let aspect = 16.0 / 9.0
        let topPadding = 6.0
        let bottomPadding = 6.0
        let leftPadding = 6.0
        let rightPadding = 6.0

        let widthDimension:NSCollectionLayoutDimension = .absolute( (itemHeight * aspect) )
        let heightDimension:NSCollectionLayoutDimension = .absolute( itemHeight )

        let itemSize = NSCollectionLayoutSize(widthDimension: widthDimension,
                                              heightDimension: heightDimension )
        
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = .init(top: topPadding, leading: leftPadding, bottom: bottomPadding, trailing: rightPadding)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: widthDimension,
                                               heightDimension: heightDimension )
        
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.contentInsets =  NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing:6)
        section.boundarySupplementaryItems = [self.makeSectionHeader()]
        section.decorationItems = [ self.makeBackgroundItem() ]
        section.supplementariesFollowContentInsets = true
        
        return section
    }
    
    func mediaLayoutSection(snapshot:NSDiffableDataSourceSnapshot<VideoAssetWrapper, SegmentEditViewWrapper>, forEnvironmentWidth:CGFloat) -> NSCollectionLayoutSection
    {
        let columnCount = Int( self.layoutSize )
        
        let itemWidth =  floor( forEnvironmentWidth / CGFloat( columnCount ) )
        
        let aspect = 9.0 / 16.0
        let topPadding = 6.0
        let bottomPadding = 6.0
        let leftPadding = 6.0
        let rightPadding = 6.0

        let itemSize = NSCollectionLayoutSize(widthDimension: .absolute( itemWidth ),
                                              heightDimension: .absolute( itemWidth  * aspect ))
        
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = .init(top: topPadding, leading: leftPadding, bottom: bottomPadding, trailing: rightPadding)

        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute( forEnvironmentWidth ),
                                               heightDimension: .absolute( itemWidth * aspect  ) )
        
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets =  NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing:6)
        section.boundarySupplementaryItems = [self.makeSectionHeader()]
        section.decorationItems = [ self.makeBackgroundItem() ]
        section.supplementariesFollowContentInsets = true

        return section
    }
        
    
    private func makeSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem
    {
        let layoutSectionHeaderItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50))
        let layoutSectionHeaderItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderItemSize, elementKind: VideoAssetHeaderView.kind, alignment: .top)
        
        layoutSectionHeaderItem.pinToVisibleBounds = true
        return layoutSectionHeaderItem
    }
    
    private func makeBackgroundItem() -> NSCollectionLayoutDecorationItem
    {
        let background = NSCollectionLayoutDecorationItem.background(elementKind: OzuBackgroundRoundedView.kind)
        background.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing:6)
        return background
    }
1

There are 1 best solutions below

0
vade On

So, interestingly, this seems to be some weird behavior with pinToVisibleBounds with orthogonal scrolling.

Disabling pinToVisibleBounds fixes some of the odd behaviors I see.