SwiftUI Mac应用菜单能否根据第一响应者/前置窗口改变状态?
在SwiftUI里实现AppKit风格的菜单项状态控制
一、让菜单项根据第一响应者自动启用/禁用
你之前用tryToPerform触发命令的方式没问题,但没法自动禁用不支持的菜单项。这里有两种靠谱的实现方式:
方式1:贴近SwiftUI的手动判断法
通过监听第一响应者的变化,动态更新菜单项的禁用状态:
struct ContentView: View { @State private var canPerformAction = false // 替换成你的目标selector let targetSelector = #selector(CustomResponder.doAction(_:)) var body: some View { Menu("操作菜单") { Button("执行操作") { NSApp.keyWindow?.firstResponder?.tryToPerform(targetSelector, with: nil) } .disabled(!canPerformAction) } // 监听第一响应者切换事件 .onReceive(NotificationCenter.default.publisher(for: NSResponder.becomeFirstResponderNotification)) { _ in updateActionAvailability() } .onReceive(NotificationCenter.default.publisher(for: NSResponder.resignFirstResponderNotification)) { _ in updateActionAvailability() } .onAppear { updateActionAvailability() } } private func updateActionAvailability() { guard let responder = NSApp.keyWindow?.firstResponder else { canPerformAction = false return } // 检查第一响应者是否支持该selector canPerformAction = responder.responds(to: targetSelector) } }
这种方式完全用Swift API实现,缺点是需要手动监听状态变化。
方式2:原生AppKit复用(更省心)
直接利用NSMenuItem的target = nil特性——AppKit会自动检查第一响应者是否支持对应action,自动处理启用/禁用。我们可以把原生NSMenuItem包装成SwiftUI组件:
struct NativeMenuItem: NSViewRepresentable { let title: String let action: Selector let keyEquivalent: String func makeNSView(context: Context) -> NSMenuItem { let item = NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent) item.target = nil // 关键:交给AppKit自动处理 return item } func updateNSView(_ nsView: NSMenuItem, context: Context) {} }
然后在菜单里直接用:
Menu("操作菜单") { NativeMenuItem(title: "执行操作", action: #selector(CustomResponder.doAction(_:)), keyEquivalent: "") }
这种方式完全复用AppKit的原生逻辑,不需要自己处理状态监听,推荐优先用这个。
二、根据第一响应者状态显示/隐藏勾选标记
同样有两种实现思路:
方式1:SwiftUI手动控制勾选图标
通过获取第一响应者的状态,动态显示勾选图标:
struct ContentView: View { @State private var isActionChecked = false let toggleAction = #selector(CustomResponder.toggleAction(_:)) let checkStatusSelector = #selector(CustomResponder.getActionChecked(_:)) var body: some View { Menu("操作菜单") { Button { // 触发切换操作 NSApp.keyWindow?.firstResponder?.tryToPerform(toggleAction, with: nil) // 立即更新勾选状态 updateCheckedStatus() } label: { HStack { Text("切换状态") Spacer() // 根据状态显示勾选图标 Image(systemName: isActionChecked ? "checkmark" : "") } } .disabled(!canPerform(toggleAction)) } .onReceive(NotificationCenter.default.publisher(for: NSResponder.becomeFirstResponderNotification)) { _ in updateCheckedStatus() } .onReceive(NotificationCenter.default.publisher(for: NSResponder.resignFirstResponderNotification)) { _ in updateCheckedStatus() } .onAppear { updateCheckedStatus() } } private func updateCheckedStatus() { guard let responder = NSApp.keyWindow?.firstResponder else { isActionChecked = false return } var status: Bool = false // 调用第一响应者的状态查询方法 if responder.responds(to: checkStatusSelector) { responder.tryToPerform(checkStatusSelector, with: &status) } isActionChecked = status } private func canPerform(_ selector: Selector) -> Bool { guard let responder = NSApp.keyWindow?.firstResponder else { return false } return responder.responds(to: selector) } }
这里需要你的第一响应者实现getActionChecked(_:)方法,用来返回当前的勾选状态,toggleAction(_:)用来切换状态。
方式2:原生NSMenuItem状态控制
通过NSMenuDelegate在菜单打开时更新NSMenuItem的state属性:
// 自定义菜单代理,负责更新勾选状态 class CheckMenuDelegate: NSObject, NSMenuDelegate { let checkSelector: Selector init(checkSelector: Selector) { self.checkSelector = checkSelector super.init() } func menuWillOpen(_ menu: NSMenu) { guard let targetItem = menu.item(withTitle: "切换状态"), let responder = NSApp.keyWindow?.firstResponder else { return } var isChecked: Bool = false if responder.responds(to: checkSelector) { responder.tryToPerform(checkSelector, with: &isChecked) } // 设置原生菜单项的勾选状态 targetItem.state = isChecked ? .on : .off } } // 包装原生勾选菜单项 struct NativeCheckMenuItem: NSViewRepresentable { let title: String let action: Selector let checkSelector: Selector private let delegate = CheckMenuDelegate(checkSelector: #selector(CustomResponder.getActionChecked(_:))) func makeNSView(context: Context) -> NSMenuItem { let item = NSMenuItem(title: title, action: action, keyEquivalent: "") item.target = nil // 设置菜单代理 item.menu?.delegate = delegate return item } func updateNSView(_ nsView: NSMenuItem, context: Context) {} }
然后在菜单里使用:
Menu("操作菜单") { NativeCheckMenuItem(title: "切换状态", action: #selector(CustomResponder.toggleAction(_:)), checkSelector: #selector(CustomResponder.getActionChecked(_:))) }
这种方式完全用AppKit原生的勾选状态逻辑,不需要自己处理图标显示。
总结下:
- 启用状态:优先用原生
NSMenuItem包装法,省心又符合AppKit的原生行为; - 勾选标记:如果想纯SwiftUI实现就手动控制图标,想贴近原生就用
NSMenuDelegate更新菜单项状态。
内容的提问来源于stack exchange,提问作者Duncan C
相关产品推荐
相关产品推荐

