I have been trying to display a custom NSMenuItem (for a preview page of a menu manager) inside a SwiftUI view. But I can't achieve it. I have figured it needs to wrapped inside a menu first, and thought that there might be a way to pop the menu pragmatically but sadly, those efforts have failed and the app crashes.
So far, my code looks like this:
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) {
}
}
}
Am I missing something here? Is it even possible to just pragmatically display NSMenuItem?
The NSMenu comfors to NSViewRepresentable so I figured it might just workout, and have seen answers on StackOverflow (granted date a while back) showing similar code that should work.
Without the popUpMenuPositioningItem
it works - in a way I guess - when I right click in the View, the MenuItem Appears. But I would like to be able to display the menu without the right click, just like that.
CodePudding user response:
The problem is that the menu is shown while the view are still rendering so that the crash happens. To avoid this you should call popUp(positioning:at:in)
after the your view appears on the screen. The way to achieve it, we have to use publisher to trigger an event to show menu inside onAppear
modifier and listen it inside Coordinator
. Here is the sample for that solution.
struct ContentView: View {
let menuPopUpTrigger = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NSMenuItemView(menuPopUpTrigger)
Text("Hello, world!")
}
.padding()
.onAppear {
/// trigger an event when `onAppear` is executed
menuPopUpTrigger.send()
}
}
}
struct NSMenuItemView: NSViewRepresentable {
let base = NSView()
let menu = NSMenu()
var menuPopUpTrigger: PassthroughSubject<Void, Never>
init(_ menuPopUpTrigger: PassthroughSubject<Void, Never>) {
self.menuPopUpTrigger = menuPopUpTrigger
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSView {
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
item.target = context.coordinator
base.menu = menu
context.coordinator.bindTrigger(menuPopUpTrigger)
return base
}
func updateNSView(_ view: NSView, context: Context) { }
}
extension NSMenuItemView {
final class Coordinator: NSObject {
var parent: NSMenuItemView
var cancellable: AnyCancellable?
init(_ parent: NSMenuItemView) {
self.parent = parent
}
@objc func valueChanged(_ sender: NSMenuItem) { }
/// bind trigger to listen an event
func bindTrigger(_ trigger: PassthroughSubject<Void, Never>) {
cancellable = trigger
.delay(for: .seconds(0.1), scheduler: RunLoop.main)
.sink { [weak self] in
self?.parent.menu.popUp(
positioning: self?.parent.menu.item(at: 0),
at: NSPoint(x: 0, y: 0),
in: self?.parent.base
)
}
}
}
}
I hope it will help you to get what you want.