vade
vade

Reputation: 762

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

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

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
    }

Upvotes: 0

Views: 127

Answers (1)

vade
vade

Reputation: 762

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

Disabling pinToVisibleBounds fixes some of the odd behaviors I see.

Upvotes: 0

Related Questions