I'm attempting to write a macOS version of https://stackoverflow.com/a/74935849/2178159.
From my understanding, I should be able to set the menu
property of an NSResponder
and it will automatically show on right click.
I've tried a couple things:
A: set menu
on an NSHostingController
's view
- when I do this and right or ctrl click, nothing happens.
B: set menu
on NSHostingController
directly - when I do this I get a crash Abstract method -[NSResponder setMenu:] called from class _TtGC7SwiftUI19NSHostingControllerGVS_21_ViewModifier_...__. Subclasses must override
C: manually call NSMenu.popup
in a custom subclasses of NSHostingController
or NSView
's rightMouseDown
method - nothing happens.
extension View {
func contextMenu(menu: NSMenu) -> some View {
modifier(ContextMenuViewModifier(menu: menu))
}
}
struct ContextMenuViewModifier: ViewModifier {
let menu: NSMenu
func body(content: Content) -> some View {
Interaction_UI(
view: { content },
menu: menu
)
.fixedSize()
}
}
private struct Interaction_UI<Content: View>: NSViewRepresentable {
typealias NSViewType = NSView
@ViewBuilder var view: Content
let menu: NSMenu
func makeNSView(context: Context) -> NSView {
let v = NSHostingController(rootView: view)
// option A - no effect
v.view.menu = menu
// option B - crash
v.menu = menu
return v.view
}
func updateNSView(_ nsView: NSViewType, context: Context) {
// part of option A
nsView.menu = menu
}
}
The view class for NSHostingController
might have overridden menu(for:)
or the menu
property, or it might have set menu
to something else (to implement SwiftUI .contextMenu { ... }
for example) after you have set it. After all, unlike in UIKit where context menus are added to the interactions
array, there is only one single menu
property to set on NSView
.
I suggest subclassing NSHostingView
and overriding menu(for:)
to return your own NSMenu
property.
class MyHost<Content: View>: NSHostingView<Content> {
var myMenu: NSMenu?
override func menu(for event: NSEvent) -> NSMenu? {
myMenu
}
}
Then the NSViewRepresentable
can be written like this:
struct ViewWithMenu<Content: View>: NSViewRepresentable {
@ViewBuilder var view: Content
let menu: NSMenu
func makeNSView(context: Context) -> NSView {
let v = MyHost(rootView: view)
v.myMenu = menu
return v
}
func updateNSView(_ nsView: NSView, context: Context) {
nsView.menu = menu
}
}
Here is a minimum example that produces a menu on right click:
struct ContextMenuViewModifier: ViewModifier {
let menu: NSMenu
func body(content: Content) -> some View {
ViewWithMenu(
view: { content },
menu: menu
)
.fixedSize()
}
}
extension View {
func contextMenu(menu: NSMenu) -> some View {
modifier(ContextMenuViewModifier(menu: menu))
}
}
struct ContentView: View {
var body: some View {
let m = {
let temp = NSMenu()
let item = NSMenuItem(title: "Foo", action: nil, keyEquivalent: "")
temp.items = [item]
return temp
}()
Text("Foo")
.contextMenu(menu: m)
}
}
In reality, you would probably hold the NSMenu
in a @Observable
/ObservableObject
that can respond to the menu items' actions. That class can also be the menu's delegate
.