0

I have a (subclassed) NSCollectionView open, containing multiple text views. Each of the text views is mapped to a (subclassed) NSDocument object. (The idea is to use the document architecture's save functions but not its windowing functions, because I need multiple documents in the same window and the traditional document architecture doesn't allow that.)

Now, there's a function I'd like the user to be able to call from the main menu that will affect their currently selected document. That is: the document is currently visible in a text view with current focus, and the menu command should make an alteration to that document. But the sender of the menu command is just the menu. When the window controller handles the command from the menu, how can I tell it what the currently selected document is?

Displaced Hoser
  • 871
  • 3
  • 13
  • 35

1 Answers1

1

This is what the responder chain is for.

Since you're using NSCollectionView, you probably already have a subclass of NSCollectionViewItem. If not, create one. Implement your action method in this subclass. Example:

class DocumentItem: NSCollectionViewItem {

    var document: MyDocument? {
        return representedObject as? MyDocument
    }

    @IBAction func doThatThing(sender: AnyObject?) {
        Swift.print("This is where I do that thing to \(document)")
    }

    // @IBOutlets and whatnot here...

}

You may need to set this as the custom class of your NSCollectionViewItem in your xib or storyboard.

Next, if your cell view (the view owned by your NSCollectionViewItem) isn't a custom subclass of NSView already, you should make it a custom subclass. You must override acceptsFirstResponder to return true:

class DocumentCellView: NSView {

    override var acceptsFirstResponder: Bool { return true }

    // @IBOutlets and whatnot here...

}

Make sure you set this as the custom class of your cell view in your storyboard or xib.

Finally, connect the action of your menu item to doThatThing: on First Responder:

connection menu item to first responder

Here's how it works:

Because the cell view now returns true for acceptsFirstResponder, when the user clicks a cell view in the collection view, the system will make it the first responder (the start of the responder chain).

When a view has a view controller, it makes that view controller the next responder after itself in the responder chain (if you are on OS X 10.10 Yosemite or later). Your cell view has a view controller: the item object you return from outlineView:itemForRepresentedObjectAtIndexPath:. (NSCollectionViewItem is a subclass of NSViewController, so your custom item is a view controller.)

When the user clicks the menu item, the menu item asks NSApplication to send its action along the responder chain, starting with the first responder. The first responder is the cell view, but it doesn't respond to the doThatThing: message. So NSApplication asks the view for its nextResponder, which is an instance of your NSCollectionViewItem subclass. That object does respond to doThatThing:, so NSApplication sends doThatThing: to your item object (with the NSMenuItem object as the sender argument) and doesn't check the rest of the responder chain.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Brilliant! Well done! In my case, the answer was actually even simpler. The NSTextViews were already set up to accept First Responder, so I didn't need to subclass them. I just needed to create the @IBAction within my CollectionViewItem subclass and connect the menu item to it on First Responder. But I would not have figured that out by myself. Major props for not only coming up with a simple answer that worked well for me, but posting the more complex variant of it that will benefit future readers of this question. – Displaced Hoser Aug 06 '16 at 00:11