collection view as collection item (nested collection views)

I have a NSCollectionView (let's call it parent) where each item is a NSCollectionView (call it child). And the child's item is just an NSImageView. In other word, it's a collection of image collection. The child collection is just for display, I don't need scroll nor selection.

However selection for the parent collection doesn't work. Its func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) doesn't get called.

Normal selection works because when I replace its item with a simple NSView, it get called.

What could cause this issue ?

Here is the code for the parent collection view controller:

class ParentViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate {
  lazy var collectionViewLayout: NSCollectionViewFlowLayout = {
  let collectionViewLayout: NSCollectionViewFlowLayout = NSCollectionViewFlowLayout.init()
  collectionViewLayout.itemSize = NSSize(width: 100.0, height: 100.0)
  collectionViewLayout.minimumLineSpacing = 10.0
  collectionViewLayout.minimumInteritemSpacing = 10.0
  collectionViewLayout.sectionInset = NSEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
  return collectionViewLayout
  }()

  lazy var collectionView: NSCollectionView = {
  let collectionView: NSCollectionView = NSCollectionView.init()
  collectionView.collectionViewLayout = self.collectionViewLayout
  collectionView.dataSource = self
  collectionView.delegate = self
  collectionView.isSelectable = true
  collectionView.backgroundColors = [NSColor.red, NSColor.red]
  return collectionView
  }()

  override func loadView() {
  let clipView: NSClipView = NSClipView.init()
  clipView.documentView = self.collectionView
  let scrollView: NSScrollView = NSScrollView.init(frame: NSRect(x: 0.0, y: 0.0, width: 500.0, height: 500.0))
  scrollView.contentView = clipView

  self.view = scrollView
  }

  override func viewDidLoad() {
  super.viewDidLoad()
  self.collectionView.register(ParentItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("ParentItemID"))
  }

  func numberOfSections(in collectionView: NSCollectionView) -> Int {
  return 1
  }

  func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
  return 50
  }

  func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
  let item: NSCollectionViewItem = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ParentItemID"), for: indexPath)
  guard let parentItem: ParentItem = item as? ParentItem else { return item }
  return parentItem
  }

  func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
  indexPaths.forEach({ print($0) })
  collectionView.deselectItems(at: indexPaths)
  }
}

Here is the child collection view controller, which is the parent item:

class ParentItem: NSCollectionViewItem, NSCollectionViewDataSource {
  lazy var collectionViewLayout: NSCollectionViewFlowLayout = {
    let collectionViewLayout: NSCollectionViewFlowLayout = NSCollectionViewFlowLayout.init()
    collectionViewLayout.estimatedItemSize = NSZeroSize
    collectionViewLayout.itemSize = NSSize(width: 27.0, height: 27.0)
    collectionViewLayout.minimumLineSpacing = 3.0
    collectionViewLayout.minimumInteritemSpacing = 3.0
    collectionViewLayout.sectionInset = NSEdgeInsetsMake(3.0, 3.0, 3.0, 3.0)
    return collectionViewLayout
  }()


  class ChildCollectionView: NSCollectionView {
    // nothing for now
  }


  lazy var mycollectionView: ChildCollectionView = {
    let mycollectionView: ChildCollectionView = ChildCollectionView.init(frame: NSRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
    mycollectionView.collectionViewLayout = self.collectionViewLayout
    mycollectionView.dataSource = self
    mycollectionView.backgroundColors = [NSColor.blue, NSColor.blue]
    return mycollectionView
  }()


  override func loadView() {
    self.view = mycollectionView
  }


  override func viewDidLoad() {
    super.viewDidLoad()
    self.mycollectionView.register(ChildItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("ChildItemID"))
  }


  func numberOfSections(in collectionView: NSCollectionView) -> Int {
    return 1
  }


  func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
    return 7
  }


  func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
    let item: NSCollectionViewItem = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ChildItemID"), for: indexPath)
    guard let childItem: ChildItem = item as? ChildItem else { return item }
    return childItem
  }
}

And finally the child item:

class ChildItem: NSCollectionViewItem {
  lazy var faviconImageView: ImageView = {
  let image: NSImage = NSImage.init(imageLiteralResourceName: "anImage")
  let faviconImageView: ImageView = ImageView.init(image: image)
  return faviconImageView
  }()

  override func loadView() {
  self.view = self.faviconImageView
  }
}

Accepted Reply

You should define the class for NSCollectionViewItem as conforming to NSCollectionViewDataSource, NSCollectionViewDelegate.


And define the delegate functions as needed.

Replies

Could you add


  func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) {
  indexPaths.forEach({ print($0) })
  }


in ParentItem


and tell what you get.

So I've finally found a solution.

In the ParentItem:

  class ChildCollectionView: NSCollectionView {
    override func mouseDown(with event: NSEvent) {
      super.mouseDown(with: event)
      self.nextResponder?.mouseDown(with: event)
    }


    override func mouseUp(with event: NSEvent) {
      super.mouseUp(with: event)
      self.nextResponder?.mouseUp(with: event)
    }
  }


  lazy var mycollectionView: ChildCollectionView = {
    let mycollectionView: ChildCollectionView = ChildCollectionView.init(frame: NSRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
    mycollectionView.collectionViewLayout = self.collectionViewLayout
    mycollectionView.dataSource = self
    mycollectionView.isSelectable = true
    mycollectionView.backgroundColors = [NSColor.blue, NSColor.blue]
    return mycollectionView
  }()

In the ParentViewController:

  func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
    let item: NSCollectionViewItem = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ParentItemID"), for: indexPath)
    guard let parentItem: ParentItem = item as? ParentItem else { return item }
    if parentItem.mycollectionView.delegate !== self { parentItem.mycollectionView.delegate = self }
    return parentItem
  }
  
  func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) {
    if collectionView === self.collectionView {
      indexPaths.forEach({ print($0) })
    }
    else {
      let center: NSPoint = collectionView.center
      if let indexPath = self.collectionView.indexPathForItem(at: center) {
        print(indexPath)
      }
    }
    collectionView.deselectItems(at: indexPaths)
  }

And a small extension in order to get the center point:

extension NSView {
  var center: NSPoint { return CGPoint(x: NSMidX(self.frame), y: NSMidY(self.frame)) }
}



BUT, I have an issue in the ParentItem, with the flow layout: It continues on a single row. It's like it doesn't know the collectionview size, in order to change rows.

Now if I use NSCollectionViewGridLayout, it displays well, but selection doesn't work anymore.

You should explain what you changed. I (we) cannot spend time comparing both versions line by line !

Yes, you’re right. So I’ve subclassed the ChildCollectionView, especially mouseDown: and mouseUp: I’ve set the ChildCollectionView delegate to the ParentCollectionView delegate. So now in collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) method, if the collection view is the parent, so normal selection is performed. Otherwise, I get the center point of the child collection view, and ask the parent collection for the corresponding indexPath. The fact that the child collection layout displays only on the first row isn’t new. It always did that. If I add a NSClipView, or if I change the flow layout by a grid layout, it displays well, but selecting won’t work.

BUT, I have an issue in the ParentItem, with the flow layout: It continues on a single row. It's like it doesn't know the collectionview size, in order to change rows.



What do you mean : It continues on a single row


I do not understand your intent:

  func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 
    if collectionView === self.collectionView { 
      indexPaths.forEach({ print($0) }) 
    } 
    else { 
      let center: NSPoint = collectionView.center 
      if let indexPath = self.collectionView.indexPathForItem(at: center) { 
        print(indexPath) 
      } 
    } 
    collectionView.deselectItems(at: indexPaths) 
  }



center is the center of collectionView, not a given cell ? Or do I miss something ?

In addintion, going to geometry to find the cellected cell which is passed in the func parameters seems clumsy.

Is it Ok to continue the subject in stackoverflow.com ? It's easier to add images (screenshot) to explain my case.

https://stackoverflow.com/questions/56946775/nscollectionview-as-nscollectionviewitem

Of course, you can start a thread in SO, we'll see you there.


Take care to explain very clearly your issue there, some readers are not very patient on SO 😉.

Ok, it's written. And yes you're right, precise and clear 😉

https://stackoverflow.com/questions/56946775/nscollectionview-as-nscollectionviewitem

Hi, did you get a chance to look at the SO question ? I'm sorry, I know you're busy.

Yes, I saw, but it seems you got the solution ?

No, it's a half solution. As I said in the end of the question, When I try to select a BlueItem: if I click on an empty space inside the BlueItem, the RedCollectionView delegate is triggered as expected, but if I click on one of the green parts inside the BlueItem, the RedCollectionView delegate isn't triggered.

The 3 points at the end of the issue are still unsolved.

You should define the class for NSCollectionViewItem as conforming to NSCollectionViewDataSource, NSCollectionViewDelegate.


And define the delegate functions as needed.