You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 22:44:56