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

SwiftUI中主动显示NSMenuItem遇崩溃,求解决方案

SwiftUI中主动弹出NSMenu崩溃,如何实现自动显示菜单?

我尝试在SwiftUI视图中显示自定义NSMenuItem(用于菜单管理器的预览页面),但未能成功。我了解到需要先将其包裹在NSMenu中,本想通过代码主动弹出菜单,但尝试后应用出现崩溃。

去掉popUpMenuPositioningItem代码后,右键点击视图时能显示菜单,但我希望无需右键操作,直接主动显示菜单。请问我遗漏了什么?是否可以通过代码主动显示NSMenuItem?

当前代码如下:

import Foundation
import SwiftUI

struct NSMenuItemView: NSViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    
    func makeNSView(context: Context) -> NSView {
            let view = NSView()
            let menu = NSMenu()
            let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
            item.target = context.coordinator
            view.menu = menu
            return view
    }

    func updateNSView(_ view: NSView, context: Context) {
        // App crashes here :/ 
        view.menu?.popUpMenuPositioningItem(
            positioning: view.menu?.item(at: 0),
            at: NSPoint(x: 0, y: 0),
            in: view
        )
    }
}

extension NSMenuItemView {
    final class Coordinator: NSObject {
        var parent: NSMenuItemView
    
        init(_ parent: NSMenuItemView) {
            self.parent = parent
        }

        @objc
        func valueChanged(_ sender: NSMenuItem) {
        }
    }
}

问题原因与解决方案

1. 崩溃原因

updateNSView方法会被SwiftUI频繁调用(比如视图状态更新、布局变化时),而且此时视图可能还没有完全加入到应用的视图层级中,直接调用popUpMenuPositioningItem会导致系统无法正确处理菜单弹出的上下文,从而引发崩溃。

2. 正确的实现方式

要主动弹出菜单,需要确保视图已经完成布局并处于活跃状态,推荐以下两种方法:

方法一:在makeNSView中延迟调用弹出

利用DispatchQueue.main.async将弹出操作推迟到下一个主循环,确保视图已经完成初始化和布局:

func makeNSView(context: Context) -> NSView {
    let view = NSView()
    let menu = NSMenu()
    let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
    item.target = context.coordinator
    view.menu = menu
    
    // 延迟弹出菜单,确保视图已就绪
    DispatchQueue.main.async {
        view.menu?.popUpMenuPositioningItem(
            view.menu?.item(at: 0),
            at: NSPoint(x: 0, y: view.bounds.height), // 调整位置避免菜单被截断
            in: view
        )
    }
    
    return view
}

方法二:通过SwiftUI的onAppear触发弹出

给NSMenuItemView添加一个绑定状态,在SwiftUI视图的onAppear中触发菜单弹出:

struct NSMenuItemView: NSViewRepresentable {
    @Binding var shouldShowMenu: Bool
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        let menu = NSMenu()
        let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
        item.target = context.coordinator
        view.menu = menu
        return view
    }

    func updateNSView(_ view: NSView, context: Context) {
        if shouldShowMenu {
            view.menu?.popUpMenuPositioningItem(
                view.menu?.item(at: 0),
                at: NSPoint(x: 0, y: view.bounds.height),
                in: view
            )
            // 重置状态避免重复弹出
            DispatchQueue.main.async {
                shouldShowMenu = false
            }
        }
    }
    
    final class Coordinator: NSObject {
        var parent: NSMenuItemView
    
        init(_ parent: NSMenuItemView) {
            self.parent = parent
        }

        @objc
        func valueChanged(_ sender: NSMenuItem) {
        }
    }
}

// 使用示例
struct ContentView: View {
    @State private var showMenu = false
    
    var body: some View {
        NSMenuItemView(shouldShowMenu: $showMenu)
            .frame(width: 100, height: 100)
            .onAppear {
                showMenu = true
            }
    }
}

额外注意点

  • 菜单弹出的位置at参数需要合理设置,比如使用view.bounds.height作为y值,避免菜单被视图底部截断;
  • 避免在updateNSView中无条件调用弹出,否则会因为视图频繁更新导致菜单反复弹出;
  • 如果需要多次主动弹出菜单,可以通过状态变量控制触发时机,确保每次只弹出一次。

内容的提问来源于stack exchange,提问作者norwegiandev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 11:15:27