NSMenu 是 macOS App 中用于构建顶栏菜单(菜单栏)或上下文菜单的类,是 AppKit 的重要组成部分。
在SwiftUI中,可以通过commands实现菜单的配置。
例如,Mac应用顶部的菜单,都是 NSMenu 和 NSMenuItem 构成。

基本结构为:
NSApplication.shared.mainMenu
├── NSMenuItem("轻压图片") → submenu: NSMenu(...)
│ ├── NSMenuItem("关于轻压图片") ...
│ ├── NSMenuItem("服务")
│ └── ...
├── NSMenuItem("文件") → submenu: NSMenu(...)
│ ├── NSMenuItem("新建窗口")
│ ├── NSMenuItem("关闭")
│ └── ...
└── ...
每个 NSMenuItem 都可以有一个 submenu(子菜单)。
基本用法
let mainMenu = NSMenu()
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
// 创建“App”菜单
let appMenu = NSMenu()
let quitItem = NSMenuItem(title: "退出 App", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
appMenu.addItem(quitItem)
appMenuItem.submenu = appMenu
// 设置为主菜单
NSApplication.shared.mainMenu = mainMenu

常用方法
1、NSMenu:菜单容器,用于顶部菜单 / 子菜单。
2、NSMenuItem:单个菜单项,类似“打开文件”。
3、submenu:子菜单,常见于多级结构,菜单项下的菜单。
4、action:方法选择器,#selector(someFunc),点击后的方法。
5、keyEquivalent:快捷键,”q” 表示 Cmd+Q,可为空。
在SwiftUI项目中添加菜单(推荐AppDelegate)
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
setupMenu()
// 监听 App 激活状态
NotificationCenter.default.addObserver(
self,
selector: #selector(reapplyMenu),
name: NSApplication.didBecomeActiveNotification,
object: nil
)
}
@objc func reapplyMenu() {
setupMenu()
}
@objc func showAbout() {
print("点击关于")
}
func setupMenu() {
let mainMenu = NSMenu()
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu(title: "App")
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "关于", action: #selector(showAbout), keyEquivalent: "")
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(withTitle: "退出", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
NSApplication.shared.mainMenu = mainMenu
}
}
@main
struct ImageSlimApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
在实际测试中发现,只有首次打开应用才会显示自定义的菜单。关闭应用,重新打开后,会还原回默认的菜单,这个问题在后续实际应用到NSMenu时,再进行处理。
同理,如果是在视图中通过按钮调用自定义菜单的功能:
struct ContentView: View {
var body: some View {
VStack {
Color.blue
}
.padding()
.contextMenu {
Button("设置菜单") {
let mainMenu = NSMenu()
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
// 创建“App”菜单
let appMenu = NSMenu()
let quitItem = NSMenuItem(title: "退出 App", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
appMenu.addItem(quitItem)
appMenuItem.submenu = appMenu
// 设置为主菜单
NSApplication.shared.mainMenu = mainMenu
}
}
}
}
也会在关闭并重新打开应用后,恢复默认的菜单栏。
也可能是 NSApplication.shared.mainMenu 仅限于临时修改菜单栏。
上下文菜单
NSMenu.popUp(positioning:at:in:) 是 macOS 中的一个非常实用的方法,用于手动弹出一个上下文菜单(弹出菜单),可以在任意位置展示它,而不依赖系统默认事件(比如 right-click)。
func popUp(positioning item: NSMenuItem?, at location: NSPoint, in view: NSView?)
参数解析:
item:可选:要高亮的菜单项,一般传 nil。
location:要弹出的位置(以 view 的坐标为基准)。
view:位置基准视图;如果为 nil,以屏幕为基准。
import SwiftUI
import AppKit
class MenuHandler: NSObject {
@objc func menuAction(_ sender: NSMenuItem) {
print("点击了菜单项:\(sender.title)")
}
}
struct ContentView: View {
let handler = MenuHandler()
var body: some View {
VStack {
Button("显示菜单") {
let menu = NSMenu()
let itemA = NSMenuItem(title: "选项 A", action: #selector(MenuHandler.menuAction(_:)), keyEquivalent: "")
itemA.target = handler // 明确指定响应对象
menu.addItem(itemA)
let itemB = NSMenuItem(title: "选项 B", action: #selector(MenuHandler.menuAction(_:)), keyEquivalent: "")
menu.addItem(itemB)
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "退出", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
// 获取当前点击的窗口位置
if let window = NSApp.keyWindow,
let contentView = window.contentView {
let mouseLocation = NSEvent.mouseLocation
let locationInWindow = window.convertPoint(fromScreen: mouseLocation)
let locationInView = contentView.convert(locationInWindow, from: nil)
// 弹出菜单
menu.popUp(positioning: nil, at: locationInView, in: contentView)
}
}
}
.padding()
}
}

需要注意的是,菜单项如果不设置响应的action对象,也就是NSMenuItem().target的话,菜单项就会变成不可点击的灰色。
在SwiftUI中,还可以通过contextMenu更简单的实现右键菜单的效果:
import SwiftUI
import AppKit
struct ContentView: View {
var body: some View {
VStack {
Color.blue
}
.padding()
.contextMenu {
Button("选项 A") {
print("点击了 A")
}
Button("选项 B") {
print("点击了 B")
}
Divider()
Button("退出") {
NSApp.terminate(nil)
}
}
}
}

总结
通过NSApplication.shared.mainMenu设置顶部菜单,NSMenuItem(title:action:keyEquivalent:)设置单个菜单。
在SwiftUI中添加菜单时,推荐使用 .commands 替代 NSMenu。同样,如果需要设置上下文菜单,推荐使用 contextMenu 替代NSMenu。
扩展知识
NSMenu和commands是什么关系?
commands 是 SwiftUI 的语法糖,最终还是生成 AppKit 的 NSMenu 菜单结构。
SwiftUI的.commands {}对应NSMenu + NSMenuItem,.CommandMenu()对应NSMenu 的一个子菜单。
SwiftUI 只是用声明式语法,自动创建了 NSMenu 结构,背后依然是 AppKit 驱动菜单栏。
例如:
// SwiftUI 中添加菜单栏
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("工具") {
Button("刷新数据") {
print("刷新数据")
}
}
}
}
}
背后做了什么?
1、创建一个 NSMenu,标题是 “工具”。
2、创建一个 NSMenuItem,标题是 “刷新数据”,action 是调用绑定的 Swift 方法。
3、注册为主菜单(mainMenu)。
可以在运行 App 时:
⌘点击菜单栏“工具”。
看到菜单项“刷新数据”。
触发 SwiftUI 中绑定的操作。
使用场景
简单固定菜单,使用SwiftUI 的 .commands 最方便。
动态生成菜单,需要使用NSMenu 手动控制。
上下文菜单,使用.contextMenu {}(SwiftUI)或 NSMenu.popUp(AppKit)。
多级嵌套、图标菜单、禁用控制,使用 NSMenu。
相关文章
1、SwiftUI macOS的commands菜单栏修饰符:https://fangjunyu.com/2025/06/19/swiftui-macos%e7%9a%84commands%e8%8f%9c%e5%8d%95%e6%a0%8f%e4%bf%ae%e9%a5%b0%e7%ac%a6/
2、SwiftUI长按手势弹出上下文菜单contextMenu:https://fangjunyu.com/2024/12/10/swift%e4%b8%8a%e4%b8%8b%e6%96%87%e8%8f%9c%e5%8d%95contextmenu/