Issue #980

NSCollectionView, available since macOS 10.5+, is a good choice to present a list of content. Let’s make a SwiftUI wrapper for NSCollectionView with diffable data source and compositional layout

Use NSViewControllerRepresentable

First, let’s create a SwiftUI view that will represent the macOS view controller. We’ll use the NSViewControllerRepresentable protocol to bridge SwiftUI and AppKit.

import SwiftUI
import AppKit

struct CollectionView: NSViewControllerRepresentable {
    func makeNSViewController(context: Context) -> CollectionViewController {
        return CollectionViewController()
    }
    
    func updateNSViewController(_ nsViewController: CollectionViewController, context: Context) {
        nsViewController.update()
    }
}

In this code, the CollectionView struct acts as a wrapper around CollectionViewController, our main view controller where all the action happens.

Creating the View Controller

Next, we’ll create the CollectionViewController, which is responsible for displaying the list of UUIDs.

final class CollectionViewController: NSViewController {
    typealias Item = String
    
    private var scrollView: NSScrollView!
    private var collectionView: NSCollectionView!
    private var dataSource: NSCollectionViewDiffableDataSource<Int, Item>!

Make it scrollable

We want our list to be scrollable, so we embed the NSCollectionView inside an NSScrollView. This allows users to scroll through the list if it’s too long to fit on the screen.

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Set up ScrollView
    scrollView = NSScrollView()
    scrollView.hasVerticalScroller = true
    view.addSubview(scrollView)
    scrollView.constrainEdges(to: view)
    
    // Set up CollectionView
    collectionView = NSCollectionView()
    collectionView.backgroundColors = [.clear]
    scrollView.documentView = collectionView
    
    // Set up Compositional Layout
    collectionView.collectionViewLayout = createCompositionalLayout()
    
    // Register Cell
    collectionView.register(CollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("UUIDItem"))
    
    // Set up Data Source
    dataSource = NSCollectionViewDiffableDataSource(
        collectionView: collectionView,
        itemProvider: { collectionView, indexPath, item in
            let cell = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier("UUIDItem"), for: indexPath) as! CollectionViewItem
            cell.textField?.stringValue = item
            return cell
        }
    )
    
    // Populate data
    update()
}

Here’s what’s happening:

  • NSScrollView: We create a scroll view and add it to the main view.
  • NSCollectionView: This is where our list items will be displayed. We set it as the documentView of the scroll view.
  • Compositional Layout: We set up a simple layout to arrange the items in the collection view.

Creating the Compositional Layout

The compositional layout allows us to create flexible and complex layouts easily. In this case, we’re keeping it simple with a vertical list.

func createCompositionalLayout() -> NSCollectionViewCompositionalLayout {
    return NSCollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
        
        // Define item size
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(30)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        // Define group size and layout
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(30)
        )
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
        group.interItemSpacing = .fixed(10)
        
        // Define section
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 10
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        
        return section
    }
}

This layout creates a simple vertical list where each item takes up the full width of the collection view and has a height of 30 points.

Adding Data to the Collection View

Finally, we need to populate our collection view with data. In this example, we’re generating 1,000 random UUID strings.

func update() {
    var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
    snapshot.appendSections([0])
    let uuidStrings = (0..<1000).map { _ in UUID().uuidString }
    snapshot.appendItems(uuidStrings, toSection: 0)
    dataSource.apply(snapshot)
}

We create an array of 1,000 random UUID strings. Then we use a snapshot to manage the data in the collection view. This allows us to efficiently update the view when the data changes.

Customizing the Collection View Item

We need to create a custom NSCollectionViewItem to display each UUID string.

class CollectionViewItem: NSCollectionViewItem {
    override func loadView() {
        self.view = NSView()
        let textField = NSTextField(labelWithString: "")
        textField.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(textField)
        NSLayoutConstraint.activate([
            textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
            textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
            textField.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        self.textField = textField
    }
}

And that’s it! You’ve built a macOS app that displays a scrollable list of 1,000 random UUID strings using Swift, AppKit, and NSCollectionView. This project demonstrates how to use modern techniques like compositional layouts and NSDiffableDataSource to create a dynamic and efficient user interface