0
votes

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)
    }
}

2

2 Answers

0
votes

The source code of a collection view in a storyboard:

<collectionView id="yh4-in-fLt">
    <rect key="frame" x="0.0" y="0.0" width="480" height="158"/>
    <autoresizingMask key="autoresizingMask" widthSizable="YES"/>
    <collectionViewFlowLayout key="collectionViewLayout" minimumInteritemSpacing="10" minimumLineSpacing="10" id="TGK-mX-O4r">
        <size key="itemSize" width="50" height="50"/>
    </collectionViewFlowLayout>
    <color key="primaryBackgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</collectionView>

but the autoresizingMask is not visible in IB. Setting autoresizingMask fixes the issue.

collectionView.autoresizingMask = [.width]
0
votes

Ignoring the frame change notification seems to do the trick.

Add a custom NSClipView as content view of the NSScrollView like so:

scrollView.contentView = ClipViewIgnoringFrameChange()

where ClipViewIgnoringFrameChange() is defined like this:

class ClipViewIgnoringFrameChange : NSClipView {
    override func viewFrameChanged(_ notification: Notification) {}
}