Hello list,
I’ve encountered a bug for the life of me I can’t work out. I’m restricted to
supporting Mojave (10.14) for the time being, so it is possible this has been
fixed / not an issue on later releases. This is a Core Data backed application,
with several Xib loaded views that are swapped in and out based on the
selection of a segmented control in the toolbar. The basic issue is that when I
call ‘reloadData()’ on a CollectionView, all the items disappear (this
typically happens if the user switches to a different view and then back again,
as, while previously loaded views are kept in memory, they are reloaded upon
switching back to the foreground - ie. this is not tabs, but separate loads of
Xib file Views). If I put a break point in an override of ‘viewWillDisappear()`
in my NSCollectionViewItem subclass, I can see the backtrace indicates that
NSCollectionView has decided to reuse the CollectionViewItem but not issued a
new one:
* NSCollectionView reloadData
* _NSCollectionViewCore reloadData
* _NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:
* __35-_NSCollectionViewCore reloadData]_block_invoke
* NSCollectionViewCore _reuseCell:notifyDidEndDisplaying:
* _reuseViewWithReuseQueue
* NSCollectionViewItem _setHiddenForReuse:
* NSView(NSInternal) _setHidden:setNeedsDisplay:
* NSView _recursiveViewWillDisappearBeacuseHidden
* NSViewController _sendViewWillDisappear
* @objc CollectionViewItem.viewWillDisappear()
* CollectionViewItem.viewWillDisappear()
In the NSCollectionView itself, I can see the items are removed from the
‘visibleItems()’ array, but the items themselves still seems to be known about
by the collection view, as they will remain in the ‘selectionIndexPaths:’ if
they were previously selected.
What’s really weird is that because we have multiple top level views, a lot of
the code is reused on different displays, including the NSArrayController
subclass that serves as the Delegate and DataSource for this NSCollectionView.
It works perfectly fine in the other case. The main difference between these
two top levels views, is that in the one that works, the Collection View is
slaved to a Table View (ie the collection view shows the items in a
relationship to the selection in the table view), where as in the one that
doesn’t work, the behaviour is inverted (the selection in the collection view
is the data source for a slaved table view). To be clear, though, both are
displaying completely different Core Data entities. I don’t think I’ve missed
anything, so as far as I can tell both NSCollectionViews are configured
identically in the two XIBs.
The last weird thing is that the problem only seems to manifest if the
application has previously been built. If I Make Clean first, the resulting
behaviour is as I would expect. If I close and reopen the application, the
behaviour is then broken. The backtrace is common to both situations, so the
problem is that something is preventing the Collection View from reissuing a
CollectionViewItem for the objects. I’m not sure where to look for that side,
other than to say that ‘viewWillAppear()’ is not called when the problem is
occurring, and does when it isn't.
I’m fairly convinced that the problem is in the Xib file. I have tried removing
everything else except the NSCollectionView and a few buttons and the problem
doesn’t seem to manifest in that case. I haven’t tried bringing in one
additional view or controller at a time to see when it eventually breaks,
because there are far too many for that to be a good debugging path. The view
with the working CollectionView is significantly simpler, if that matters. (I
suppose a solution could be to take the Collection View out into its own Xib
file and load into the view manually, or create it programatically, neither of
which is particularly attractive.) Doing a side-by-side comparison of the
configuration of Collection View in each Xib file and associated Controllers
only shows expected differences (e.g. entities and spacing sizes).
I’ve been banging my head agains this for several days now, so would appreciate
any guidance towards a solution,
Arved
P.S. The code is fairly straightforward, I think:
class FlowLayout: NSCollectionViewFlowLayout {
init(withSize size: NSSize, edge: CGFloat, spacing: CGFloat) {
super.init()
itemSize = size
estimatedItemSize = NSZeroSize
sectionInset = NSEdgeInsetsMake(edge, edge, edge, edge)
minimumInteritemSpacing = spacing
minimumLineSpacing = spacing
}
override func layoutAttributesForElements(in rect: NSRect) ->
[NSCollectionViewLayoutAttributes] {
// Excluded for brevity
return leftAlignedAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) ->
Bool {
return true
}
}
class CollectionViewItem: NSCollectionViewItem {
fileprivate var prefSize: NSSize
required override init(nibName nibNameOrNil: NSNib.Name?, bundle
nibBundleOrNil: Bundle?) {
prefSize = NSMakeSize(0, 0)
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
}
override var isSelected: Bool {
didSet {
super.isSelected = isSelected
if isSelected {
view.layer?.backgroundColor =
NSColor.selectedControlColor.cgColor
} else {
view.layer?.backgroundColor = NSColor.clear.cgColor
}
}
}
override var preferredContentSize: NSSize {
get {
return prefSize
}
set(newValue) {
self.prefSize = newValue
}
}
}
protocol CollectionViewDelegate : NSCollectionViewDelegate {
func collectionViewPreviewSelectedItem()
}
class CollectionView : NSCollectionView {
override func keyDown(with event: NSEvent) {
if event.characters == " " {
if let _ = self.delegate as? CollectionViewDelegate {
(self.delegate as?
CollectionViewDelegate)?.collectionViewPreviewSelectedItem()
}
} else {
super.keyDown(with: event)
}
}
}
// Abstract parent class
class CollectionViewController: NSArrayController, NSCollectionViewDataSource,
NSCollectionViewDelegateFlowLayout, CollectionViewDelegate,
ObjectControllerExtensions {
var itemSize: NSSize {
get {
return NSZeroSize
}
}
var interfaceID: NSUserInterfaceItemIdentifier {
get {
return .init("")
}
}
@IBOutlet weak var mainCollection: NSCollectionView!
@IBOutlet weak var sourceView: NSView!
override func awakeFromNib() {
super.awakeFromNib()
mainCollection.register(NSNib.init(nibNamed: interfaceID.rawValue,
bundle: nil), forItemWithIdentifier: interfaceID)
mainCollection.collectionViewLayout = FlowLayout(withSize: itemSize,
edge: 5, spacing: 10)
mainCollection.registerForDraggedTypes([.fileURL, .png, .tiff])
mainCollection.setDraggingSourceOperationMask(.every, forLocal: true)
mainCollection.setDraggingSourceOperationMask(.delete, forLocal: false)
sourceView.wantsLayer = true
}
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: NSCollectionView,
numberOfItemsInSection section: Int) -> Int {
let objects = arrangedObjects as! Array<NSManagedObject>
return objects.count
}
func collectionView(_ collectionView: NSCollectionView,
itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = mainCollection.makeItem(withIdentifier: interfaceID, for:
indexPath) as! CollectionViewItem
let objects = arrangedObjects as! Array<NSManagedObject>
item.representedObject = objects[indexPath.item]
return item
}
func collectionView(_ collectionView: NSCollectionView, layout
collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath:
IndexPath) -> NSSize {
return itemSize
}
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt
indexPaths: Set<IndexPath>) {
let objects = arrangedObjects as! Array<NSManagedObject>
let selected = indexPaths.map({path in
return objects[path.item]
})
setSelectedObjects(selected)
}
func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt
indexPaths: Set<IndexPath>) {
let objects = arrangedObjects as! Array<NSManagedObject>
let selected = indexPaths.map({path in
return objects[path.item]
})
removeSelectedObjects(selected)
}
// Removed Drag and Drop support code for brevity
}
// Working Subclass
class ImageItemController: CollectionViewController {
override var interfaceID: NSUserInterfaceItemIdentifier {
get {
return .init("ImageItemView")
}
}
override var itemSize: NSSize {
get {
return NSMakeSize(200, 200)
}
}
override func awakeFromNib() {
super.awakeFromNib()
sortDescriptors = [NSSortDescriptor(key: #keyPath(ImageItem.filename),
ascending: true)]
}
override func add(_ sender: Any?) {
// Removed for brevity
}
override func remove(_ sender: Any?) {
// Removed for brevity
}
override func collectionView(_ collectionView: NSCollectionView, layout
collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath:
IndexPath) -> NSSize {
let objects = arrangedObjects as! Array<NSManagedObject>
var size = (objects[indexPath.item].value(forKey:
#keyPath(ImageItem.image)) as? NSImage)?.size ?? itemSize
let ratio = size.height / size.width
size.width = itemSize.height / ratio
size.height = itemSize.height
return size
}
// Code removed for brevity (displaying a popover when the space bar is
pressed to show a larger version of the image)
}
// Failing Subclass
class MediaCollectionController: CollectionViewController {
override var interfaceID: NSUserInterfaceItemIdentifier {
get {
return .init("MediaCollectionView")
}
}
override var itemSize: NSSize {
get {
return NSMakeSize(146, 206)
}
}
override func awakeFromNib() {
super.awakeFromNib()
sortDescriptors = [NSSortDescriptor(key:
#keyPath(MediaCollection.title), ascending: true)]
}
override func add(_ sender: Any?) {
// Removed for brevity
}
override func remove(_ sender: Any?) {
// For testing purposes, changed this function to do nothing but reload
the collection view
mainCollection.reloadData()
}
}
_______________________________________________
Cocoa-dev mailing list ([email protected])
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com
This email sent to [email protected]