NSScrollView encapsulating NSCollectionView always scrolling back when resizing - cocoa

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

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]

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

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.

Related

Increasing `collectionViewContentSize` width causes CollectionView Cells to disappear

So I have an issue I've been battling with for days now. I have a collectionView which is designed to have large cells (width-wise) which go off screen. The height is fixed. The idea is to basically scroll to the end (see below):
To achieve being able to scroll left and right, I've had to override the width inside collectionViewContentSize in my flowLayout. Like below.
override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
size.width = largeWidth
return size
}
This achieves increasing the horizontal scroll area (which is what I want) but the cells start to disappear once I reach a certain point. It's almost as if the cells are being dequeued when they shouldn't be. Any ideas on this. This is the final straw for my project but I'm out of ideas.
Many thanks
Code snippet can be found below. You should be able to just copy and paste this into any project:
class HomeViewController: UIViewController {
let collectionView: UICollectionView
let collectionViewLayout = CustomCollectionViewFlowLayout()
init() {
collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
super.init(nibName: nil, bundle: nil)
collectionView.backgroundColor = UIColor.red.withAlphaComponent(0.4)
collectionView.register(SomeCell.self, forCellWithReuseIdentifier: "SomeCell")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView.delegate = self
collectionView.dataSource = self
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.frame = view.bounds
view.addSubview(collectionView)
}
}
class SomeCell: UICollectionViewCell {
}
extension HomeViewController: UICollectionViewDataSource,
UICollectionViewDelegate,
UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 150
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SomeCell", for: indexPath) as! SomeCell
cell.backgroundColor = .blue
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 10000, height: 70)
}
}
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
size.width = 10000
return size
}
}

macOS statusBarApp without Storyboard create and close settingsWindow causing EXC_BAD_ACCESS

I have a MacOS cocoa statusBarApp without any Storyboard with main.swift file.
The statusBarIcon shows up a Menu which presents a custom view with a button, which should open a settingsWindow - which it does. If I close the settingsWindow and reopen it and close it again, I got a EXC_BAD_ACCESS Error. It seems, that the window is deallocate but the reference is still present. I don't know how to fix this.
Edit the question like Willeke´s advice:
Thx, to your answer. Ok, hier is a minimal reproducible example:
create a new Xcode project, with storyboard and swift for macOS app.
Under Project-Infos / General / Deployment Info: Delete the main entry to the storyboard. Then delete the storyboard file itself.
Under Info set the "application is agent" flag to yes, so the app is statusBarApp only.
then you only need the code below.
The Exception Breakpoint leads to this line:
settingsWindow = NSWindow(
To reproduce the error: start the app, click on statusItem, click on menuItem, a window opens, close the window, click again all first steps and reopen the window. sometimes that's the point of crash. sometimes a few more attempts of closing the window are necessary, but not more then three times.
main.swift
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
AppDelegate.swift
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var settingsWindow: NSWindow!
var statusItemMain: NSStatusItem?
var menuMain = NSMenu()
var menuItemMain = NSMenuItem()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
statusItemMain = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let itemImage = NSImage(systemSymbolName: "power", accessibilityDescription: nil)
itemImage?.isTemplate = true
statusItemMain?.button?.image = itemImage
menuItemMain.target = self
menuItemMain.isEnabled = true
menuItemMain.action = #selector(createWindow)
menuMain.addItem(menuItemMain)
menuMain.addItem(.separator())
statusItemMain?.menu = menuMain
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow.makeKeyAndOrderFront(nil)
settingsWindow?.contentViewController = ViewController()
}
}
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func loadView() {
self.view = NSView(frame: NSRect(x: 0, y: 0, width: 750, height: 500))
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
NSWindow is released when it is closed. Before ARC this was a usefull feature. It can be switched off by setting the isReleasedWhenClosed property to false. But then the window stays in memory when it is closed because the settingsWindow property is holding on to it. Implement delegate method windowWillClose and set settingsWindow to nil so window is released.
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var settingsWindow: NSWindow!
// other methods
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.isReleasedWhenClosed = false
settingsWindow.delegate = self
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow?.contentViewController = ViewController()
settingsWindow.makeKeyAndOrderFront(nil)
}
func windowWillClose(_ notification: Notification) {
settingsWindow = nil
}
}

How to do horizontally autoscroll for colletionViewCell?

I have a collectionView at the top of my App view and I want to display some ads inside it,
I made a ready array for some pictures that I want to display them in the collection view.
Now I'm looking for a method that I can make them horizontally scroll automatically.
class HomeVC: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionViewCell()
}
#IBOutlet weak var collectionView: UICollectionView!
var array = [ "https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcR8aTilga7j-8GfP4OUXUx1bV3E2EJaFt29QdSBD8OgcLBLCUiG&usqp=CAU?",
"https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcRVq4AqZIDQ_UPnSd-PxQGkEutZnlu76NbZ4xETWelVnULLJ614&usqp=CAU",
"https://scontent.ffjr1-2.fna.fbcdn.net/v/t1.0-9/72577_563558293672959_39183751_n.jpg?_nc_cat=101&_nc_sid=dd9801&_nc_ohc=YySniLzpf_sAX-B8YFE&_nc_ht=scontent.ffjr1-2.fna&oh=12da3f0ae2404066684d742b4f785cfc&oe=5ED9FCDA",
"https://img.particlenews.com/image.php?type=thumbnail_1024x576&url=2zUI4r_0OJWDY4z00",
"https://content3.jdmagicbox.com/comp/def_content/advertising_agencies/default-advertising-agencies-9.jpg",
"https://miro.medium.com/max/895/1*2gq5_jgNSYJnLzCqIIYVGA.jpeg"]
}
Here below my registered cell that I want to make the autoscroll inside it by infinite array looping .
So can you help me how to do that?
extension HomeVC: UICollectionViewDelegate, UICollectionViewDataSource {
func setupCollectionViewCell () {
collectionView.delegate = self ; collectionView.dataSource = self
collectionView.register(UINib(nibName: "homeAdCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "adCell")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewlayout: UICollectionViewLayout, sizeForItemAt IndexPath: IndexPath) -> CGSize {
print ("Size func called")
return CGSize(width: 250, height: 75)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "adCell", for: indexPath) as! homeAdCollectionViewCell
cell.update(SURL: array[indexPath.row])
return cell
}
}
Set the Timer in ViewDidLoad and scroll to next item using collectionView.ScrollToitem method
If you want infinite scrolling with autoscroll effect, you can check this library:
https://github.com/WenchaoD/FSPagerView

Additional view in NSCollectionViewItem pauses dragEvent in CollectionViewController

I am trying to implement drop delegates on a NSCollectionViewController and having issues using a custom NSCollectionViewItem with an additional View Layer I've added onto the CollectionView Item. FWIW, The additional view is used draw a dashed border to indicate a drop area.
The drag event works fine on this collectionItem, and all other collectionItems without this view when it is hidden, but as soon as the drag event occurs on top of this view, the drag event pauses.
The drag event resumes as soon as the mouse is dragged outside of the view, but nothing happens if I release the drag while the mouse is over the view.
I would love to know what is happening here and how to prevent the custom view from "stealing" the mouse event from the CollectionViewContoller.
Delegate Method on DropViewController
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
print("1")
if proposedDropIndexPath.pointee.item <= self.destinationDirectoryArray.count {
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
} else if proposedDropIndexPath.pointee.item == self.destinationDirectoryArray.count {
//There's some stuff here validating the URL removed for brevity. It works okay when the focus is outside the view, but happy to add back in if helpful
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
}
return[]
}
Configuring Collection View
func configureCollectionView() {
let flowLayout = NSCollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 8.0
flowLayout.minimumLineSpacing = 8.0
destinationCollectionView.delegate = self
destinationCollectionView.dataSource = self
destinationCollectionView.register(NSNib(nibNamed: "DestinationCollectionItem", bundle: nil), forItemWithIdentifier: directoryItemIdentifier)
destinationCollectionView.collectionViewLayout = flowLayout
destinationCollectionView.registerForDraggedTypes([.fileURL])
destinationCollectionView.setDraggingSourceOperationMask(NSDragOperation.move, forLocal: true)
}
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
}
}
Custom Border View - Applied custom class in Xib and linked to File's Owner
class BorderedView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path : NSBezierPath = NSBezierPath(roundedRect: self.bounds, xRadius: 10.0, yRadius: 10.0)
path.addClip()
let dashHeight: CGFloat = 2
let dashLength: CGFloat = 7
let dashColor: NSColor = .lightGray
// setup the context
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.setLineWidth(dashHeight)
currentContext.setLineDash(phase: 0, lengths: [dashLength])
currentContext.setStrokeColor(dashColor.cgColor)
// draw the dashed path
let cgPath : CGPath = CGPath(roundedRect: NSRectToCGRect(self.bounds), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)
currentContext.addPath(cgPath)
currentContext.strokePath()
}
}
Well - I solved this one pretty quick.
While I previously tried adding unregisterDraggedTypes() to the backgroundLayer, the issue turned out to also be occurring on the image layer. I applied it to both the Image and backgroundLayer and it works now.
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
backgroundLayer.unregisterDraggedTypes()
self.imageView?.unregisterDraggedTypes()
self.textField?.unregisterDraggedTypes()
}
}

displaying CollectionViewController in Playground

I am simply trying to display a CollectionView in playground to quickly try out ideas. Yet this has been a hour+ affair. I'm getting the error:
2017-03-17 17:28:37.862 uikitTest[88414:10270872] * Assertion failure in -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/UICollectionView.m:4971
2017-03-17 17:28:37.866 uikitTest[88414:10270872] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'could not dequeue a view of kind: UICollectionElementKindCell with identifier test1 - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
*** First throw call stack:
I tried setting a view to the PlaygroundPage, and also creating a UIWindow. It isn't clear to me how to work with ViewControllers in Playground, and even whether this is the root problem and if its connected to the could not dequeue exception.
My Playground is as follows:
import UIKit
import XCPlayground
import PlaygroundSupport
class CVCCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
label.frame = bounds
}
}
class CVC: UICollectionViewController {
var datasource = ["1","2","3"]
override func viewDidLoad() {
view.backgroundColor = .red
print(collectionView)
collectionView?.register(CVCCell.self, forCellWithReuseIdentifier: "test1")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datasource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "test1", for: indexPath) as! CVCCell
cell.label.text = datasource[indexPath.row]
return cell
}
}
let containerView = UIView(frame: CGRect(x: 0.0,
y: 0.0,
width: 375.0,
height: 667.0))
containerView.backgroundColor = .green
let fl = UICollectionViewFlowLayout()
let cvc = CVC(collectionViewLayout: fl)
let w = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
w.rootViewController = cvc
print(cvc.view)
containerView.addSubview(cvc.view)
//PlaygroundPage.current.liveView = containerView

Resources