肇鑫的技术博客

业精于勤,荒于嬉

The Real Reason of Set NSMenuDelegate in Interface Builder Not Working

When implemented the "Open Recent..." menu item under "File" menu, I connected the menu of "Open Recent"'s delegate to "AppDelegate" class. However, the delegate functions never called.

nsmenudelegate_issue_with_interface_builder

I tried to connect the NSMenuDelegate with the main menu, the result was the same.

I looked this issue up in stackoverflow site. There are many questions on this. Someone says that "Menu delegates are not used that often, so Apple hasn't made them too easy to set up in Interface Builder. Instead, do this in awakeFromNib:".

I don't accept the theory. But the answer does imply that setting NSMenuDelegate in code works.

Then I looked up on how to implementing "Open Recent..." menu. I found this post, Respond to Open Recent clicks in NSMenu. I was glad that I could use NSDocumentController to get the feature of "Open Recent...".

But the routine approach is not suit for my app.

For apps like Pages or Numbers, the files they open varies every time on names. However, for Xliff Tool, the files it opens are exported by Xcode, and the names are fixed. So when using the default approach of NSDocumentController, the recent files shown may be the same.

open_recent_issue

As you can see in above picture, there are two files with the same name. In fact they are in different paths. I need to show the files in full paths instead of just filenames.

So the question is back again. I have to make functions to conform to NSMenuDelegate.

What the magic Apple does to make the "Open Recent..." menu working?

Apple must have implemented its own class that conforms to NSMenuDelegate protocol. Since in Interface Builder I could not find any, I would debug it on AppDelegate's applicationDidFinishLaunching(_:) function.

I connect a @IBOutlet of menu of "Open Recent..." to "AppDelegate", and found that when app runs, the openRecentMenu has been set a NSMenuDelegate called NSDocumentControllerSubMenuDelegate.

open_recent_menu_outlet

After another digging, I found that NSDocumentControllerSubMenuDelegate is a hidden class that should not be used by third-party developers.

Answer

The answer of NSMenuDelegate set in Interface Builder not working is that for mechanism of magic like "Open Recent...", Apple resets the NSMenuDelegate of all menus under an app's main menu.

Anyone who wants to use a NSMenuDelegate, should set it in code.

My Own Solution

After set the NSMenuDelegate, the rest is easy.

extension AppDelegate:NSMenuDelegate {
    func menuNeedsUpdate(_ menu: NSMenu) {
        let clearMenuMenuItem = menu.items.last!
        let urls = NSDocumentController.shared.recentDocumentURLs
        let menuItems = urls.map {
            NSMenuItem(title: $0.path, action: #selector(openFile(_:)), keyEquivalent: "")
        }
        
        menu.items = [
            menuItems,
            [NSMenuItem.separator(), clearMenuMenuItem]
        ].flatMap({$0})
    }
}

Others

The differences between @ojbc and @IBAction through an interesting bug/feature of Interface Builder.