yesthisisjoe yesthisisjoe - 6 months ago 89
Swift Question

validateMenuItem or menuWillOpen not called for NSMenu

My Mac app has an NSMenu whose delegate functions

are never called. So far none of the solutions online have helped.

It seems like I'm doing everything right:

  • The menu item's selectors are in the same class.

  • The class managing it inherits from NSMenuDelegate

I suppose the best way to describe my problem is to post the relevant code. Any help would be appreciated.

import Cocoa

class UIManager: NSObject, NSMenuDelegate {
var statusBarItem = NSStatusBar.system().statusItem(withLength: -2)
var statusBarMenu = NSMenu()
var titleMenuItem = NSMenuItem()
var descriptionMenuItem = NSMenuItem()

// ...

override init() {


// ...

func createStatusBarMenu() {
// Status bar icon
guard let icon = NSImage(named: "iconFrame44")
else { NSLog("Error setting status bar icon image."); return }
icon.isTemplate = true
statusBarItem.image = icon

// Create Submenu items
let viewOnRedditMenuItem = NSMenuItem(title: "View on Reddit...", action: #selector(viewOnRedditAction), keyEquivalent: "")
let saveThisImageMenuItem = NSMenuItem(title: "Save This Image...", action: #selector(saveThisImageAction), keyEquivalent: "")

// Add to title submenu
let titleSubmenu = NSMenu(title: "")

// Create main menu items
titleMenuItem = NSMenuItem(title: "No Wallpaperer Image", action: nil, keyEquivalent: "")
titleMenuItem.submenu = titleSubmenu
getNewWallpaperMenuItem = NSMenuItem(title: "Update Now", action: #selector(getNewWallpaperAction), keyEquivalent: "")
let preferencesMenuItem = NSMenuItem(title: "Preferences...", action: #selector(preferencesAction), keyEquivalent: "")
let quitMenuItem = NSMenuItem(title: "Quit Wallpaperer", action: #selector(quitAction), keyEquivalent: "")

// Add to main menu
let statusBarMenu = NSMenu(title: "")
statusBarMenu.addItem(quitMenuItem) = statusBarMenu

// ...

// Called whenever the menu is about to show. we use it to change the menu based on the current UI mode (offline/updating/etc)
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
NSLog("Validating menu item")
if (menuItem == getNewWallpaperMenuItem) {
if wallpaperUpdater!.state == .Busy {
DispatchQueue.main.async {
self.getNewWallpaperMenuItem.title = "Updating Wallpaper..."
return false
} else if wallpaperUpdater!.state == .Offline {
DispatchQueue.main.async {
self.getNewWallpaperMenuItem.title = "No Internet Connection"
return false
} else {
DispatchQueue.main.async {
self.preferencesViewController.updateNowButton.title = "Update Now"
return true

return true

// Whenever the menu is opened, we update the submitted time
func menuWillOpen(_ menu: NSMenu) {
NSLog("Menu will open")
if !noWallpapererImageMode {
DispatchQueue.main.async {
self.descriptionMenuItem.title = "Submitted \(self.dateSimplifier(self.updateManager!.thisPost.attributes.created_utc as Date)) by \(self.updateManager! to /r/\(self.updateManager!.thisPost.attributes.subreddit)"

// ...

// MARK: User-initiated actions

func viewOnRedditAction() {
guard let url = URL(string: "\(updateManager!.thisPost.permalink)")
else { NSLog("Could not convert post permalink to URL."); return }

// Present a save panel to let the user save the current wallpaper
func saveThisImageAction() {
DispatchQueue.main.async {
let savePanel = NSSavePanel()

savePanel.nameFieldStringValue = self.updateManager! + ".png"
let result = savePanel.runModal()

if result == NSFileHandlingPanelOKButton {
let exportedFileURL = savePanel.url!
guard let lastImagePath = UserDefaults.standard.string(forKey: "lastImagePath")
else { NSLog("Error getting last post ID from persistent storage."); return }
let imageData = try! Data(contentsOf: URL(fileURLWithPath: lastImagePath))
if (try? imageData.write(to: exportedFileURL, options: [.atomic])) == nil {
NSLog("Error saving image to user-specified folder.")

func getNewWallpaperAction() {
updateManager!.refreshAndReschedule(userInitiated: true)

func preferencesAction() {

func quitAction() {


menuWillOpen: belongs to the NSMenuDelegate protocol; for it to be called the menu in question needs a delegate:

let statusBarMenu = NSMenu(title: "")
statusBarMenu.delegate = self

validateMenuItem: belongs to the NSMenuValidation informal protocol; for it to be called the relevant menu items must have a target. The following passage is taken from Apple's Application Menu and Pop-up List Programming Topics documentation:

When you use automatic menu enabling, NSMenu updates the status of every menu item whenever a user event occurs. To update the status of a menu item, NSMenu first determines the target of the item and then determines whether the target implements validateMenuItem: or validateUserInterfaceItem: (in that order).

let myMenuItem = NSMenuItem() = self
myMenuItem.action = #selector(doSomething)