Of the three approaches already mentioned, setting UserDefaults
is probably the cleanest, though, as noted, it relies on undocumented UserDefaults
entries.
The approach of removing the items by the index position (ie, the last position) in the menu relies on the assumption that they will always be at the end of the menu. That's probably a safe assumption, but Apple could do something else the future.
Relying on the menu item names is also problematic for localization reasons.
It's better to use the selector, as shown for Objective-C, but it won't work quite so simply in Swift, particularly for the "Start Dictation..." menu item. The method you need the selector for is startDictation(_:)
, but unlike in Objective-C you can't just type it like that. You need to specify an @objc
type to which it belongs. So just search Apple's documentation, right? Good luck. It's undocumented, and the method isn't exposed in Swift.
My approach to solving that problem is to add a stub for that method in my AppDelegate
, and then use that to get the selector. You really just need some type Swift can get its teeth into to form the selector. But when the selector is tossed over the wall to the Obj-C side of Cocoa, that type just disappears. All that matters is the method's name, how many parameters, their order, and names. In this case, like most action methods, it takes one parameter (for the sender):
@objc public func startDictation(_: Any) { }
The AppDelegate
is handy for a quick and dirty implementation, but for real use I prefer to create a class that inherits for NSObject
with private
initializers specifically for stub methods like that. That way you ensure that they never pollute the responder chain. Basically make a non-instantiable bag of do-nothing methods you can use to make selectors.
Now we need a way to find the corresponding menu item, so I make an extension on NSMenu
public extension NSMenu
{
func lastMenuItem(where condition: (NSMenuItem) -> Bool) -> NSMenuItem?
{
for item in items.reversed()
{
if let submenu = item.submenu
{
if let foundItem = submenu.lastMenuItem(where: condition) {
return foundItem
}
}
else if condition(item) { return item }
}
return nil
}
}
I choose to search the menu in reverse, under the assumption that if I decided to add my own items at some future date, they will most likely be before the items I'm removing. Then in AppDelegate
:
func removeUnwantedAutomaticMenus()
{
let unwantedActions: [Selector] =
[
#selector(AppDelegate.startDictation(_:)),
#selector(NSApplication.orderFrontCharacterPalette(_:)),
]
for action in unwantedActions {
NSApp.mainMenu?.lastMenuItem { $0.action == action }?.isHidden = true
}
}
As you can see, I choose to hide the item rather than remove them, but you could certainly remove them instead. All that's left is just to call removeUnwantedAutomaticMenus()
in AppDelegate.applicationDidFinishLaunching
.
If you create your menus programmatically, you can make this more robust by using the tag
property in NSMenuItem
to mark items you added, then check for that tag to make sure you don't remove/hide those instead of the automatically added ones.
Another approach is to subclass NSMenu
, overriding its addItem
and insertItem
methods to check the NSMenuItem
's tag before adding them. Simply don't add/insert any NSMenuItem
s with an incorrect tag, which Apple's automatically inserted items won't have. If you use Storyboards for your menus, it's kind of pain, because you have to make sure the class for each menu is set to your custom class, and the tag for each menu item is set correctly. If you create your menus programmatically it's much easier to ensure everything is set correctly.