Search code examples
cocoansscrollviewnscollectionview

NSScrollView encapsulating NSCollectionView always scrolling back when resizing


The program opens a window containing a collection view with 1000 items. If I scroll a bit down, then resize the window by whatever amount in any direction, the scroll view will immediately jump back to the top.

The application is built without Storyboard or XIB. The problem does not occur when building a similar app via Interface Builder. There seems to be something missing, something Interface Builder configures by default. I am testing this with Xcode 12.4 on macOS 11.2.3. Any ideas?

To make it easy to reproduce I have packed everything in a file main.swift.:

import Cocoa

let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

class AppDelegate: NSObject, NSApplicationDelegate, NSSplitViewDelegate {
    var window: NSWindow?
    var dataSource: CollectionViewDataSource?
    var collectionView: NSCollectionView?
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let screenSize = NSScreen.main?.frame.size ?? NSSize(width: 1920, height: 1080)
        window = NSWindow(contentRect: NSMakeRect(screenSize.width/4, screenSize.height/4, screenSize.width/2, screenSize.height/2),
                          styleMask: [.miniaturizable, .closable, .resizable, .titled],
                          backing: .buffered,
                          defer: false)
        window?.makeKeyAndOrderFront(nil)
        
        if let view = window?.contentView {
            collectionView = NSCollectionView(frame: NSZeroRect)
            dataSource = CollectionViewDataSource()
            let scrollView = NSScrollView(frame: NSZeroRect)
            view.addSubview(scrollView)
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                scrollView.topAnchor.constraint(equalTo: view.topAnchor),
                scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
            scrollView.documentView = collectionView
            
            collectionView?.collectionViewLayout = NSCollectionViewFlowLayout()
            collectionView?.register(CollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CollectionViewItem"))
            collectionView?.dataSource = dataSource
        }
    }
}

class CollectionViewDataSource: NSObject, NSCollectionViewDataSource {
    func numberOfSections(in collectionView: NSCollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
        return 1000
    }
    
    func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
        let i = collectionView.makeItem(withIdentifier:NSUserInterfaceItemIdentifier(rawValue: "CollectionViewItem"), for:indexPath) as! CollectionViewItem
        i.view.wantsLayer = true
        i.view.layer?.backgroundColor = NSColor.init(colorSpace: NSColorSpace.deviceRGB, hue: CGFloat(Float.random(in: 0..<1)), saturation: CGFloat(Float.random(in: 0.4...1)), brightness: CGFloat(Float.random(in: 0.5...1)), alpha: 1).cgColor
        return i
    }
    
    func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView {
        fatalError("Not implemented")
    }
}

class CollectionViewItem : NSCollectionViewItem {
    override func loadView() {
        self.view = NSView(frame: NSZeroRect)
    }
}


Solution

  • Move the line

    collectionView?.collectionViewLayout = NSCollectionViewFlowLayout()
    

    before the line

    scrollView.documentView = collectionView
    

    Had the same problem on my own project and this fixed it. Tested with your code above and fix works there as well.