Search code examples
swiftswiftuiappkitnsscrollviewnscollectionview

How can I get scrolling to work for an NSCollectionView hosted in SwiftUI?


I've built an NSCollectionView wrapper using NSViewRepresentable, but it refuses to scroll. The class looks something like this:

final class SwiftNSCollectionView: NSObject, NSViewRepresentable, NSCollectionViewDataSource // etc
{
// ... init ...

    typealias NSViewType = NSCollectionView
    
    func makeNSView(context: Context) -> NSCollectionView {
        let collectionView = NSCollectionView()
        scrollView.documentView = collectionView
        
        updateNSView(collectionView, context: context)
        
        return collectionView
    }
    
    func updateNSView(_ scrollView: NSCollectionView, context: Context) {
        collectionView.dataSource = self
        // ... other collectionView setup
    }

    // ...
}

Typically, NSCollectionView has a built-in NSScrollView.

I've tried:

  • No wrapper—the NSCollectionView simply doesn't scroll.

  • Wrapping this SwiftNSCollectionView in a SwiftUI ScrollView, but that causes two problems:

    • The height of the the NSCollectionView collapses to 0 (which I can work around somewhat using a GeometryReader)
    • The NSCollectionView doesn't want to extend to the height of all its objects (which makes sense because it virtualizes them)
  • Using NSCollectionViewCompositionalLayoutConfiguration with an orthogonal scroll direction (hacky):

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .continuous
    
    // If the "official" scroll direction is horizontal,
    // then the orthogonal direction becomes vertical,
    // and we can scroll our one section 😈
    let configuration = NSCollectionViewCompositionalLayoutConfiguration()
    configuration.scrollDirection = .horizontal
    

    But this seemed to mess up keyboarding: after a certain number of elements, arrow keys between elements either stopped working altogether or moved to the completely wrong elements. Seemed like a virtualization bug of some sort.


Solution

  • You can mitigate this by creating an NSScrollView manually.

    1. Update the NSViewType to be NSScrollView.
    2. Update function signatures as required.
    3. Use your existing NSCollectionView as the .documentView of the new scroll view.

    Then you can use your SwiftNSCollectionView directly in SwiftUI code and it will scroll properly without any custom work on your side.

    final class SwiftNSCollectionView: NSObject, NSViewRepresentable, NSCollectionViewDataSource // etc {
        // ... init ...
    
        // No longer NSCollectionView
        typealias NSViewType = NSScrollView
        
        func makeNSView(context: Context) -> NSScrollView {
            // Create an NSScrollView, too!
            let scrollView = NSScrollView()
            let collectionView = NSCollectionView()
            scrollView.documentView = collectionView
            
            updateNSView(scrollView, context: context)
            
            return scrollView
        }
        
        func updateNSView(_ scrollView: NSScrollView, context: Context) {
            // Since we get an NSScrollView, get the child!
            let collectionView = scrollView.documentView as! NSCollectionView
            collectionView.dataSource = self
            // ... other collectionView setup
        }
    
        // ...
    }